Skip to content

Enhance documentation workflow and add Blazor WASM playground support#30

Merged
koenbeuk merged 6 commits intomainfrom
feat/docs-facelift
Apr 13, 2026
Merged

Enhance documentation workflow and add Blazor WASM playground support#30
koenbeuk merged 6 commits intomainfrom
feat/docs-facelift

Conversation

@koenbeuk
Copy link
Copy Markdown
Collaborator

Revamp documentation to include actual examples and introduce an interactive Blazor WASM playground for better user engagement and learning.

Copilot AI review requested due to automatic review settings April 13, 2026 00:21
await _initLock.WaitAsync();
try
{
if (_initialized) return;
Comment on lines +49 to +52
catch
{
return snippet;
}
Comment on lines +108 to +111
catch (Exception ex)
{
return RenderResult.Exception(Unwrap(ex));
}
Comment on lines +259 to +262
catch (Exception ex)
{
return RenderResult.Exception(Unwrap(ex));
}
foreach (var instance in _scenarioInstances.Values)
{
try { await instance.DisposeAsync(); }
catch { /* swallow — page unload */ }
Comment on lines +161 to +164
catch (Exception ex)
{
return RenderResult.Exception(Unwrap(ex));
}
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-sample blocks 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.

Comment on lines +24 to +51
/// <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;
}
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +160 to +178
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;
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_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.

Copilot uses AI. Check for mistakes.
Comment on lines +73 to +89
// 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;
}
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +32 to +40
// 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);

Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
```

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.
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +41
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'
}
})
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 13, 2026

Codecov Report

❌ Patch coverage is 58.82353% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/ExpressiveSharp/Services/ExpressiveResolver.cs 61.53% 3 Missing and 2 partials ⚠️
...iveSharp.Generator/Emitter/ReflectionFieldCache.cs 50.00% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!


private static IWebshopQueryRoots BuildMongoRoots()
{
var db = new MongoClient("mongodb://localhost:27017").GetDatabase("playground");
Comment on lines +141 to +148
catch (Exception ex)
{
targets[renderTarget.Id] = new RenderedTarget(
renderTarget.Label,
renderTarget.OutputLanguage,
FormatErrorMessage(ex),
IsError: true);
}
Comment on lines +183 to +188
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>
public void Dispose()
{
try { Instance.DisposeAsync().AsTask().GetAwaiter().GetResult(); }
catch (Exception) { /* best-effort */ }
Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 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>
@koenbeuk koenbeuk merged commit 2a61696 into main Apr 13, 2026
6 checks passed
@koenbeuk koenbeuk deleted the feat/docs-facelift branch April 13, 2026 01:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants