Enhance documentation workflow and add Blazor WASM playground support#30
Enhance documentation workflow and add Blazor WASM playground support#30
Conversation
| await _initLock.WaitAsync(); | ||
| try | ||
| { | ||
| if (_initialized) return; |
| catch | ||
| { | ||
| return snippet; | ||
| } |
| catch (Exception ex) | ||
| { | ||
| return RenderResult.Exception(Unwrap(ex)); | ||
| } |
| catch (Exception ex) | ||
| { | ||
| return RenderResult.Exception(Unwrap(ex)); | ||
| } |
| foreach (var instance in _scenarioInstances.Values) | ||
| { | ||
| try { await instance.DisposeAsync(); } | ||
| catch { /* swallow — page unload */ } |
| catch (Exception ex) | ||
| { | ||
| return RenderResult.Exception(Unwrap(ex)); | ||
| } |
There was a problem hiding this comment.
Pull request overview
This PR revamps the documentation experience by introducing pre-rendered, real compile-checked examples and adding an interactive Blazor WebAssembly playground that lets readers experiment with ExpressiveSharp queries and see provider-specific translations.
Changes:
- Added a docs prerenderer that compiles
::: expressive-sampleblocks and emits per-page JSON outputs consumed by a custom VitePress component/tab UI. - Introduced a Blazor WASM playground (Monaco editor + Roslyn services) and a shared “webshop” sample model/scenario used by both the playground and docs samples.
- Updated core/runtime pieces to better support dynamic assembly scenarios (cache resets, rescanning registries) and improved MongoDB query inspection.
Reviewed changes
Copilot reviewed 83 out of 85 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/ExpressiveSharp/Services/ExpressiveResolver.cs | Adds cache reset + assembly scan filtering/rescanning for doc/prerender scenarios. |
| src/ExpressiveSharp.MongoDB/Infrastructure/ExpressiveMongoQueryable.cs | Adds ToString() to surface MongoDB pipeline text without executing the query. |
| src/ExpressiveSharp.Generator/Emitter/ReflectionFieldCache.cs | Improves generic method overload disambiguation when emitting reflection lookups. |
| src/Docs/Prerenderer/SampleExtractor.cs | Extracts ::: expressive-sample blocks (snippet + optional setup) from markdown. |
| src/Docs/Prerenderer/Program.cs | CLI entrypoint that scans docs, compiles samples, and writes JSON outputs. |
| src/Docs/Prerenderer/LocalPlaygroundReferences.cs | Loads Roslyn metadata references from local build outputs for prerendering. |
| src/Docs/Prerenderer/ExpressiveSharp.Docs.Prerenderer.csproj | New prerenderer project and its dependencies. |
| src/Docs/PlaygroundModel/Scenarios/Webshop/WebshopDbContext.cs | EF Core model used by the playground/docs “webshop” scenario. |
| src/Docs/PlaygroundModel/Scenarios/Webshop/Product.cs | Webshop entity type for samples. |
| src/Docs/PlaygroundModel/Scenarios/Webshop/OrderStatus.cs | Webshop enum used in samples. |
| src/Docs/PlaygroundModel/Scenarios/Webshop/Order.cs | Webshop entity type for samples. |
| src/Docs/PlaygroundModel/Scenarios/Webshop/LineItem.cs | Webshop entity type for samples. |
| src/Docs/PlaygroundModel/Scenarios/Webshop/IWebshopQueryRoots.cs | Multi-root query context passed into snippets (db.Customers, db.Orders, etc.). |
| src/Docs/PlaygroundModel/Scenarios/Webshop/Customer.cs | Webshop entity type for samples. |
| src/Docs/PlaygroundModel/ExpressiveSharp.Docs.PlaygroundModel.csproj | New PlaygroundModel project (net10). |
| src/Docs/Playground.WasmWorkspaceShim/NoOpPersistentStorageConfiguration.cs | WASM shim to bypass Roslyn persistent storage PNSE via MEF export. |
| src/Docs/Playground.WasmWorkspaceShim/MSSharedLib1024.snk.txt | Documents the checked-in public signing key and why it’s needed. |
| src/Docs/Playground.WasmWorkspaceShim/MSSharedLib1024.snk | Public key used for public signing/IVT impersonation. |
| src/Docs/Playground.WasmWorkspaceShim/ExpressiveSharp.Docs.Playground.WasmWorkspaceShim.csproj | Shim project config (assembly name impersonation + public signing). |
| src/Docs/Playground.Wasm/wwwroot/js/monaco-interop.js | JSInterop layer for Monaco (replacing BlazorMonaco). |
| src/Docs/Playground.Wasm/wwwroot/app.htm | Standalone host HTML for the playground (theme sync, deep links, Monaco loader). |
| src/Docs/Playground.Wasm/Services/RoslynMonacoConverters.cs | Converts Roslyn completion/hover outputs into Monaco DTOs. |
| src/Docs/Playground.Wasm/Services/PlaygroundReferences.cs | Fetches reference DLLs in WASM to build Roslyn MetadataReferences. |
| src/Docs/Playground.Wasm/Services/PlaygroundLanguageServices.cs | Long-lived Roslyn workspace to drive completions/hovers. |
| src/Docs/Playground.Wasm/Services/MonacoTypes.cs | Monaco DTO types for JSInterop serialization. |
| src/Docs/Playground.Wasm/Services/MonacoMarkerConverter.cs | Converts snippet diagnostics into Monaco marker (squiggle) data. |
| src/Docs/Playground.Wasm/Properties/launchSettings.json | Local run profiles for the WASM project. |
| src/Docs/Playground.Wasm/Program.cs | WASM bootstrapping, custom element registration, and Monaco provider wiring. |
| src/Docs/Playground.Wasm/ExpressiveSharp.Docs.Playground.Wasm.csproj | Playground WASM project configuration + dependencies + lazy-load. |
| src/Docs/Playground.Wasm/Components/PlaygroundHost.razor.css | Styling for the embedded playground component UI. |
| src/Docs/Playground.Wasm/_Imports.razor | Razor imports for the WASM project. |
| src/Docs/Playground.Core/Services/SnippetFormatter.cs | Formats snippet chains for readability (line-breaking member access chains). |
| src/Docs/Playground.Core/Services/SnippetCompiler.cs | Compiles snippets via Roslyn + generators, loads generated assembly, tracks spans. |
| src/Docs/Playground.Core/Services/Scenarios/WebshopScenarioInstance.cs | In-memory EF Core contexts per scenario instance (SQLite + optional Postgres). |
| src/Docs/Playground.Core/Services/Scenarios/WebshopScenario.cs | Defines the “webshop” scenario wrapper template, references, and render targets. |
| src/Docs/Playground.Core/Services/Scenarios/ScenarioRenderTarget.cs | Render target abstraction (label, output language, render function, lazy-load). |
| src/Docs/Playground.Core/Services/Scenarios/ScenarioRegistry.cs | Registry for available scenarios and default selection. |
| src/Docs/Playground.Core/Services/Scenarios/IScenarioInstance.cs | Scenario instance contract (async dispose + query argument). |
| src/Docs/Playground.Core/Services/Scenarios/IPlaygroundScenario.cs | Scenario contract (wrapper template, refs, targets, factory). |
| src/Docs/Playground.Core/Services/IPlaygroundReferences.cs | Shared reference-loading contract for WASM and prerenderer. |
| src/Docs/Playground.Core/ExpressiveSharp.Docs.Playground.Core.csproj | Core playground services project (net10) shared by WASM + prerenderer. |
| ExpressiveSharp.slnx | Adds the new Docs projects to the solution. |
| docs/reference/troubleshooting.md | Updates docs content/links (includes EF Core integration link change). |
| docs/reference/pattern-matching.md | Replaces static snippets with ::: expressive-sample blocks. |
| docs/reference/null-conditional-rewrite.md | Replaces static snippets with ::: expressive-sample blocks and updated text. |
| docs/reference/expressive-for.md | Replaces static snippets with ::: expressive-sample blocks and updated examples. |
| docs/reference/expressive-attribute.md | Replaces static snippets with ::: expressive-sample blocks and updated examples. |
| docs/recipes/window-functions-ranking.md | Fixes recipe links to be relative. |
| docs/recipes/reusable-query-filters.md | Converts examples to ::: expressive-sample blocks and updates wording. |
| docs/recipes/nullable-navigation.md | Converts examples to ::: expressive-sample blocks and updates wording. |
| docs/recipes/modern-syntax-in-linq.md | Converts examples to ::: expressive-sample blocks and updates wording. |
| docs/recipes/external-member-mapping.md | Converts examples to ::: expressive-sample blocks and updates wording/links. |
| docs/recipes/dto-projections.md | Converts examples to ::: expressive-sample blocks and updates wording/links. |
| docs/playground-editor.md | Adds a dedicated docs page embedding the playground via iframe. |
| docs/index.md | Updates landing page messaging and adds MongoDB package mention. |
| docs/guide/window-functions.md | Updates integration link path. |
| docs/guide/migration-from-projectables.md | Converts examples to ::: expressive-sample blocks and updates sample content. |
| docs/guide/introduction.md | Expands provider-agnostic positioning + adds MongoDB references. |
| docs/guide/integrations/mongodb.md | New MongoDB integration guide page. |
| docs/guide/integrations/ef-core.md | Renames/updates EF Core guide and converts examples to ::: expressive-sample. |
| docs/guide/integrations/custom-providers.md | New custom providers guide page. |
| docs/guide/extension-members.md | Converts examples to ::: expressive-sample blocks and updates examples. |
| docs/guide/expressive-queryable.md | Converts examples to ::: expressive-sample blocks and reorganizes content. |
| docs/guide/expressive-properties.md | Converts examples to ::: expressive-sample blocks and reframes for providers. |
| docs/guide/expressive-methods.md | Converts examples to ::: expressive-sample blocks and updates examples. |
| docs/guide/expressive-constructors.md | Converts examples to ::: expressive-sample blocks and updates examples. |
| docs/guide/expression-polyfill.md | Converts examples to ::: expressive-sample blocks and updates examples. |
| docs/advanced/custom-transformers.md | Converts examples to ::: expressive-sample blocks and updates one snippet. |
| docs/.vitepress/theme/index.ts | Registers the new ExpressiveSample Vue component. |
| docs/.vitepress/theme/custom.css | Minor CSS change (newline). |
| docs/.vitepress/theme/components/ExpressiveSample.vue | New tabbed sample component rendering pre-highlighted input/output + playground link. |
| docs/.vitepress/plugins/expressive-sample.ts | Markdown-it plugin turning ::: expressive-sample into ExpressiveSample components. |
| docs/.vitepress/config.mts | Adds plugins/middleware for samples + serving playground assets + sidebar/nav updates. |
| Directory.Packages.props | Adds pinned Roslyn Features + Blazor WASM package versions + EF Sqlite.Core pin. |
| .gitignore | Ignores generated docs sample data and published playground artifacts. |
| .github/workflows/docs.yml | Builds/publishes playground, prerenders samples, then builds/deploys VitePress site. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// <summary> | ||
| /// Clears all process-level caches built up by the resolver. Intended for test harnesses | ||
| /// and the docs prerenderer, where many short-lived assemblies are loaded in sequence and | ||
| /// accumulated <c>[ExpressiveFor]</c> registrations across them would cause false "multiple | ||
| /// mappings" errors. Not part of the public production API surface. | ||
| /// </summary> | ||
| public static void ResetAllCaches() | ||
| { | ||
| _assemblyRegistries.Clear(); | ||
| _expressionCache.Clear(); | ||
| _reflectionCache.Clear(); | ||
| _lastScannedAssemblyCount = 0; | ||
| _assemblyScanFilter = null; | ||
| } | ||
|
|
||
| private static Func<Assembly, bool>? _assemblyScanFilter; | ||
|
|
||
| /// <summary> | ||
| /// Restricts <see cref="EnsureAllRegistriesLoaded"/> to assemblies matching the given filter. | ||
| /// Used by the docs prerenderer to register only the currently-rendering snippet's assembly | ||
| /// instead of every previously-loaded snippet assembly still in the AppDomain. | ||
| /// Pass <c>null</c> to remove the filter. | ||
| /// </summary> | ||
| public static void SetAssemblyScanFilter(Func<Assembly, bool>? filter) | ||
| { | ||
| _assemblyScanFilter = filter; | ||
| _lastScannedAssemblyCount = 0; | ||
| } |
There was a problem hiding this comment.
ResetAllCaches() and SetAssemblyScanFilter() are declared public, which makes them part of ExpressiveSharp's public API surface (and a long-term compatibility commitment). This conflicts with the XML docs stating they're not production APIs. Consider making these internal (or moving behind a test-only/Prerenderer-specific hook), or at minimum hide them from IntelliSense (e.g., EditorBrowsable(Never)) and clearly document thread-safety/usage constraints.
| private static int _lastScannedAssemblyCount; | ||
| private static readonly object _scanLock = new(); | ||
|
|
||
| /// <summary> | ||
| /// Scans all loaded assemblies once to discover expression registries. | ||
| /// This is a one-time cost on the first <see cref="FindExternalExpression"/> call. | ||
| /// Scans loaded assemblies for expression registries. Rescans on demand | ||
| /// whenever new assemblies have been loaded into the AppDomain since the | ||
| /// previous scan — this matters for runtime-compiled assemblies (e.g. | ||
| /// the docs prerenderer) where the first scan happens before later | ||
| /// samples' assemblies are loaded. | ||
| /// </summary> | ||
| private static void EnsureAllRegistriesLoaded() | ||
| { | ||
| if (_allRegistriesScanned) return; | ||
| var assemblies = AppDomain.CurrentDomain.GetAssemblies(); | ||
| if (assemblies.Length == _lastScannedAssemblyCount) return; | ||
|
|
||
| lock (_scanLock) | ||
| { | ||
| if (_allRegistriesScanned) return; | ||
| assemblies = AppDomain.CurrentDomain.GetAssemblies(); | ||
| if (assemblies.Length == _lastScannedAssemblyCount) return; |
There was a problem hiding this comment.
_lastScannedAssemblyCount is read outside the _scanLock but is not declared volatile and not accessed via Volatile.Read/Write. On multi-threaded workloads this can lead to stale reads (skipping a needed rescan) or other memory-ordering issues. Consider making the field volatile or using Volatile.Read(ref _lastScannedAssemblyCount) / Volatile.Write(...) when comparing/updating it.
| // Try the standard _framework/ path first. If Blazor's BaseAddress | ||
| // doesn't point at the playground subdirectory (e.g., the web component | ||
| // is hosted on a VitePress page), fall back to the playground/ prefix. | ||
| var url = $"_framework/{assemblyName}.dll"; | ||
| try | ||
| { | ||
| var bytes = await _http.GetByteArrayAsync(url); | ||
| return MetadataReference.CreateFromImage(bytes, filePath: assemblyName + ".dll"); | ||
| } | ||
| catch (HttpRequestException) | ||
| { | ||
| // The runtime sometimes splits an assembly into multiple package | ||
| // ones — if a logical name doesn't resolve to a file in /_framework, | ||
| // skip it. Roslyn will surface a "missing reference" diagnostic | ||
| // later if the snippet actually needs the type. | ||
| return null; | ||
| } |
There was a problem hiding this comment.
The comment says FetchAsync will "fall back to the playground/ prefix" if BaseAddress isn't rooted at the playground, but the implementation only attempts _framework/{assemblyName}.dll once and returns null on HttpRequestException. Either implement the documented fallback attempt (e.g., try a second URL) or update the comment so it matches the actual behavior.
| // Register Monaco completion + hover providers via our JS interop module. | ||
| // The DotNetObjectReference callbacks dispatch to PlaygroundLanguageServices. | ||
| var runtime = host.Services.GetRequiredService<PlaygroundRuntime>(); | ||
| var jsRuntime = host.Services.GetRequiredService<IJSRuntime>(); | ||
|
|
||
| var providerRef = DotNetObjectReference.Create(new MonacoLanguageProviderBridge(runtime)); | ||
| await jsRuntime.InvokeVoidAsync("monacoInterop.registerCompletionProvider", providerRef); | ||
| await jsRuntime.InvokeVoidAsync("monacoInterop.registerHoverProvider", providerRef); | ||
|
|
There was a problem hiding this comment.
DotNetObjectReference is created and passed to JS but never disposed. Even though this is app-lifetime in WASM, disposing it (e.g., via using var providerRef = ... or hooking into host shutdown) avoids leaking the managed handle and makes the pattern safer if this initialization is ever moved/repeated.
docs/reference/troubleshooting.md
Outdated
| ``` | ||
|
|
||
| See [EF Core Integration](../guide/ef-core-integration) for the full setup guide. | ||
| See [EF Core Integration](../guid./integrations/ef-core) for the full setup guide. |
There was a problem hiding this comment.
Broken link: ../guid./integrations/ef-core has a typo and will 404. It should point to the EF Core integration page under ../guide/integrations/ef-core.
docs/playground-editor.md
Outdated
| onMounted(() => { | ||
| const frame = document.getElementById('playground-frame') | ||
| if (!frame) return | ||
|
|
||
| const base = location.origin + '/ExpressiveSharp/_playground/app.htm' | ||
| const theme = isDark() ? 'dark' : 'light' | ||
| const hash = location.hash || '' | ||
| frame.setAttribute('src', `${base}?theme=${theme}${hash}`) | ||
|
|
||
| // Auto-resize iframe to fit its content | ||
| window.addEventListener('message', (e) => { | ||
| if (e.data?.type === 'playground-resize') { | ||
| frame.style.height = e.data.height + 'px' | ||
| } | ||
| }) |
There was a problem hiding this comment.
The message event listener added in onMounted is never removed, so navigating away from this page can leave a dangling handler (and potentially multiple handlers if the page is remounted). Store the handler function and call window.removeEventListener('message', handler) from onUnmounted (you can also register onUnmounted once at the top level rather than inside onMounted).
…CI, docs, and release workflows
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
|
|
||
| private static IWebshopQueryRoots BuildMongoRoots() | ||
| { | ||
| var db = new MongoClient("mongodb://localhost:27017").GetDatabase("playground"); |
| catch (Exception ex) | ||
| { | ||
| targets[renderTarget.Id] = new RenderedTarget( | ||
| renderTarget.Label, | ||
| renderTarget.OutputLanguage, | ||
| FormatErrorMessage(ex), | ||
| IsError: true); | ||
| } |
| catch (Exception ex) | ||
| { | ||
| targets[id] = new RenderedTarget(label, language, | ||
| FormatErrorMessage(ex), | ||
| IsError: true); | ||
| } |
- Make ResetAllCaches / SetAssemblyScanFilter internal; add InternalsVisibleTo for the prerenderer so they don't leak into the public NuGet surface. - Use Volatile.Read/Write on _lastScannedAssemblyCount for the double-checked scan path. - Fix stale "fallback to playground/ prefix" comment in PlaygroundReferences.FetchAsync. - Fix broken link ../guid./integrations/ef-core in reference/troubleshooting.md. - Register the playground-editor message handler with a stored reference and unregister it in onUnmounted so it doesn't leak on navigation. - Type the best-effort catch in ScenarioInstanceScope.Dispose as catch (Exception). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
⚠️ Performance Alert ⚠️
Possible performance regression was detected for benchmark 'ExpressiveSharp Benchmarks'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.50.
| Benchmark suite | Current: af542e3 | Previous: a28453c | Ratio |
|---|---|---|---|
ExpressiveSharp.Benchmarks.ExpressionReplacerBenchmarks.Replace_Property |
2994.7634136058664 ns (± 62.797282412415214) |
1633.7744944645808 ns (± 22.19090086860079) |
1.83 |
ExpressiveSharp.Benchmarks.ExpressionReplacerBenchmarks.Replace_Method |
3023.652741065392 ns (± 38.81474360780265) |
1625.9702588594878 ns (± 11.943613554726635) |
1.86 |
ExpressiveSharp.Benchmarks.ExpressionReplacerBenchmarks.Replace_NullConditional |
5314.392727887189 ns (± 31.545261891896605) |
2648.0482689429973 ns (± 55.01195303767202) |
2.01 |
ExpressiveSharp.Benchmarks.ExpressionReplacerBenchmarks.Replace_BlockBody |
5887.068582388071 ns (± 73.19108234836135) |
3281.5870919063173 ns (± 58.215268872526664) |
1.79 |
ExpressiveSharp.Benchmarks.ExpressionReplacerBenchmarks.Replace_DeepChain |
17935.2975365775 ns (± 129.06842521841486) |
8731.344099121094 ns (± 119.78224028163278) |
2.05 |
ExpressiveSharp.Benchmarks.TransformerBenchmarks.ExpandExpressives_FullPipeline |
17538.65412394206 ns (± 133.60937864020283) |
8687.791273328992 ns (± 91.25863132823535) |
2.02 |
This comment was automatically generated by workflow using github-action-benchmark.
- codecov.yml: ignore src/Docs/** alongside the existing tests/, benchmarks/, samples/, docs/ ignores. - [ExcludeFromCodeCoverage] on ExpressiveResolver.ResetAllCaches and SetAssemblyScanFilter — internal helpers that only run in the docs prerenderer harness, so they shouldn't pull down patch coverage on the core library. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Revamp documentation to include actual examples and introduce an interactive Blazor WASM playground for better user engagement and learning.