Extensions packages, pinnable filter bar, and v3.1 release prep - #41
Open
teociaps wants to merge 36 commits into
Open
Extensions packages, pinnable filter bar, and v3.1 release prep#41teociaps wants to merge 36 commits into
teociaps wants to merge 36 commits into
Conversation
- Introduce new extension packages for Swashbuckle and NSwag - Add API Counter feature to append operation counts to tag descriptions - Implement Swashbuckle filter and NSwag processor with extension methods - Provide sample projects and controllers demonstrating usage - Add comprehensive unit tests for filters, processors, and extensions - Update README, contributing guide, and add package docs - Add GitHub issue and PR templates for bugs, features, and questions - Update solution and project files to include new extensions and tests - Ensure compatibility with .NET 8, 9, and 10
- Introduce new extension packages for Swashbuckle and NSwag - Add API Counter feature to append operation counts to tag descriptions - Implement Swashbuckle filter and NSwag processor with extension methods - Provide sample projects and controllers demonstrating usage - Add comprehensive unit tests for filters, processors, and extensions - Update README, contributing guide, and add package docs - Add GitHub issue and PR templates for bugs, features, and questions - Update solution and project files to include new extensions and tests - Ensure compatibility with .NET 8, 9, and 10
…I.Themes into new-extensions
ThemeSwitcher.s_registeredThemes/s_scannedTypes and FileProvider.s_registeredEndpoints are process-wide static fields mutated during UseSwaggerUI() setup without synchronization, so concurrent host startups in the same process (e.g. parallel test collections spinning up WebApplicationFactory instances) could corrupt state or throw. Switch them to ConcurrentDictionary-backed storage and replace every ContainsKey-then-indexer or HashSet.Add check-then-act with atomic TryAdd.
…able AddGetEndpoint used to register one MapGet (WebApplication) or one app.Use middleware (classic pipelines) per theme asset, so every unrelated request walked an ever-growing chain as themes were added. Now a single middleware is registered once per app instance and does an O(1) lookup against a shared dispatch table, for both WebApplication and IApplicationBuilder hosts alike.
…ion auth bypass The single-middleware dispatch table introduced for classic IApplicationBuilder pipelines was keyed off a single process-wide dictionary, so any app instance with the dispatch middleware installed could serve paths registered by a completely different app instance in the same process. The dispatch content is now stored per app instance (via ConditionalWeakTable), while the existing path registry stays as the process-wide duplicate-registration guard only. Also reinstates the WebApplication-specific MapGet(...).AllowAnonymous() registration path, since routing already gives WebApplication O(1)-ish lookup and only MapGet's endpoint metadata can bypass a global fallback authorization policy - a bare middleware has no equivalent, so collapsing it into the shared IApplicationBuilder dispatch middleware silently broke themes for any consumer with a "secure everything by default" auth setup.
Reload previously reset the topbar to its default pinned state; the pin button's preference is now saved and restored on load, applying the class directly at init to avoid a flash of the wrong state.
…tensions-v* tags deploy.ps1 was hardcoded to only pack *.Themes projects, so the newly added *.Extensions packages were never packed or pushed by the release pipeline. Add a mandatory -Filter param (Themes|Extensions) that fails fast on an invalid or missing value, and have the workflow derive the filter from the release tag prefix (themes-v*/extensions-v*), failing the step explicitly for any other tag.
…nction Extract the pinned/unpinned class-icon-title logic into a single applyPinnedState helper shared by the initial-load restore and the click handler, instead of duplicating it, mirroring the theme switcher's switchTheme pattern. Also restores the ui.min.js BOM lost in the previous regeneration to keep the file's encoding consistent.
…ntion Pass the release tag through env: instead of splicing it straight into the bash run: body, avoiding the classic Actions script-injection pattern. Update build.yml's tag trigger from the old v*.*.* pattern to themes-v*.*.*/extensions-v*.*.*, since it would otherwise never match a release tag again once the new convention lands and silently stop running Build/Test on releases. Also make deploy.ps1's -Filter check case-sensitive so a wrong-case value fails loudly instead of producing a glob that silently matches no projects.
Adds a PinnableFilterBar advanced option that lets consumers keep Swagger UI's operation filter box visible while scrolling, mirroring the existing pinnable-topbar feature (same JS pin-button/localStorage-persistence shape, defaulting to unpinned since this is a new opt-in feature). The filter bar's own DOM wrapper is exactly as tall as the bar itself, so naively making it position:sticky has no visible effect (a sticky box is bounded by its containing block, which needs spare height to actually "stick" through a scroll). The nearest ancestor with real height doesn't carry a stable class in Swagger UI's markup, so it's targeted structurally via :has(). Offsets contributed by the pinned topbar and pinned filter bar are now exposed as CSS custom properties and summed via calc() to correctly position the sticky operation tag header under any combination of pinned topbar/filter bar.
…on, and DRY up pin-toggle JS Corrects an inaccurate comment about which element the filter-bar sticky :has() selector actually targets (the div's own containing block is one level further up than the comment claimed), adds the CSS treatment the new filter-bar pin button was missing (cursor, hover/active feedback, icon color, pinned-state animation, matching the topbar's pin button), and extracts the topbar/filter-bar pin-toggle logic (button creation, click handling, localStorage persistence) into one shared helper instead of two near-identical copies. Also centralizes the pinned-topbar/filter-bar height constants into single :root declarations so the DOM-branch-specific copies required by the :has() offset-propagation trick can't drift apart, and recalibrates the filter bar's height now that its pin button has real padding/margin affecting layout.
Give NSwag consumers a way to enable the swagger-ui bundle's native
operation filter box, matching Swashbuckle's typed EnableFilter().
NSwag's AdditionalSettings dictionary is spliced as raw key: value
text into the same SwaggerUIBundle({...}) config object the bundle
itself reads, so the raw "filter" key works identically. The
bool-overload also wires in the pinnable filter bar from
EnablePinnableFilterBar().
Lets Swashbuckle consumers enable the operation filter box and, in the same call, make the filter bar pinnable via the pin behavior built into the pinnable-filter-bar feature.
Match the non-destructive idiom the rest of this dictionary relies on so a user-provided AdditionalSettings["filter"] value (e.g. a filter expression string) isn't silently clobbered by EnableFilter().
Adds direct unit tests against SwaggerUIOptions to exercise the pin/no-pin branch that decides whether the filter bar's pinnable option gets registered, since the existing suite only covered the underlying dictionary extension it calls into.
…t in package readmes
…ilter bar The topbar's and filter bar's pin buttons had byte-for-byte identical base/hover/active/pinned rules copy-pasted between them, differing only in the icon fill color and the topbar's extra translate on its pinned state. Combine the shared declarations into grouped selectors instead.
…nce styles and update docs
…ackages Restyles both Extensions packages' NuGet docs to match the Themes packages' format: emoji section headers, quick-start/features/usage layout in package-readme.md, and the boxed release-notes format in readme.txt (now v1.0, describing the initial API Counter feature).
…d Packages A Swashbuckle user has no use for the NSwag-only Extensions package, and vice versa. Keep only the same-generator Themes companion link, which covers a genuinely different concern for the same audience.
Pinnable filter bar, persisted pinnable topbar state, and the other v3.1 changes documented in the readme.txt release notes ship as a minor bump.
AppendOperationCountToTags() and its filter/processor were leftover in AspNetCore.SwaggerUI.Themes and NSwag.AspNetCore.Themes from before the Extensions packages existed as their own projects - an exact duplicate of what now lives in AspNetCore.SwaggerUI.Extensions and NSwag.AspNetCore.Extensions. Never shipped in a published Themes release, so removing it isn't a breaking change. Drops the corresponding calls from the two Themes sample projects, which don't reference the Extensions packages; the dedicated Sample.AspNetCore.SwaggerUI.Extensions / Sample.NSwag.AspNetCore.Extensions samples already demonstrate the feature.
Follow-up to a186a4f: these two samples reference the Themes packages only (not Extensions), so the AppendOperationCountToTags() call no longer compiles once the duplicated feature is removed from Themes. The dedicated Extensions samples already demonstrate it.
'dotnet test tests/*' relied on there being exactly one directory under tests/ - now that AspNetCore.Swagger.Extensions.Tests sits alongside AspNetCore.SwaggerUI.Themes.Tests, the shell glob expands to two paths and dotnet test rejects multiple project arguments (MSB1008). Loop over the glob and test each project separately.
Both pull_request triggers listed [opened, reopened, edited] but not synchronize, so pushing new commits to an already-open PR never re-ran CI - it only re-triggered on PR metadata edits. This is why the tests/* glob fix in the previous commit didn't get validated by its own PR.
Replaces the long-lived NUGET_API_KEY secret with NuGet/login@v1, which exchanges the job's OIDC token for a short-lived API key at publish time. Requires a Trusted Publishing policy configured on nuget.org for this repo/workflow, plus a NUGET_USER secret holding the nuget.org username (not email). deploy.ps1 needs no change - it already accepts the API key as a plain parameter.
…s' version Versioning.props' shared Debug-config fallback (Version + the implicit GeneratePackageOnBuild=true) tracks the Themes packages' version line (currently 3.x) and both Extensions csproj files only overrode their own 1.0.0 version for Release, so any plain 'dotnet build' (default Debug) silently auto-packed AspNetCore.SwaggerUI.Extensions / NSwag.AspNetCore.Extensions stamped with the Themes version instead of their own. Add an explicit Debug-config override to both, matching the pattern already used for Release. Verified: dotnet msbuild -t:GetVersion now reports 1.0.0-<rev> in Debug (was 3.1.0-<rev>) and 1.0.0 in Release for both packages, and a locally packed nupkg loads and executes AppendOperationCountToTagDescriptionFilter.Apply() correctly end to end.
… 10.x / net10.0) Microsoft.OpenApi 2.x is a breaking rewrite (Models namespace removed, OpenApiDocument.Tags and OpenApiOperation.Tags collection types changed, OpenApiTagReference replaces OpenApiTag on operations) that ships with Swashbuckle.AspNetCore 10.x. AppendOperationCountToTagDescriptionFilter is now multi-targeted via #if NET10_0_OR_GREATER to compile correctly against both API shapes, and AspNetCore.SwaggerUI.Extensions.csproj takes a TFM-conditional Swashbuckle.AspNetCore reference (9.x for net8.0/net9.0, 10.x for net10.0) with upper-bounded ranges so NuGet can't silently float a TFM onto the wrong major line again. Updates the filter's test suite to build the corresponding v1/v2 object graphs per TFM, and relaxes the empty-document assertion since OpenApiDocument.Tags defaults to null in v2 vs. an empty collection in v1 - both are correct given the filter never touches Tags when there's nothing to append. Verified: dotnet test passes 22/22 on net8.0, net9.0, and net10.0; a locally packed nupkg was smoke-tested end to end against real Swashbuckle.AspNetCore 10.2.3 on net10.0.
Swashbuckle.AspNetCore 10.x's bundled swagger-ui-dist ships its own dark-mode toggle in the topbar, layering swagger-ui's own dark CSS on top of whatever theme is active - visibly clashing with it (confirmed in-browser: operation blocks change color/border once both are active). This library's own theme, plus EnableThemeSwitcher when enabled, already cover light/dark, so hide the native toggle. No-op on older Swashbuckle versions that don't render the element at all. Verified in-browser against Swashbuckle.AspNetCore 10.2.3: toggle no longer renders in the topbar; pin button and theme switcher unaffected.
Two bugs surfaced by Swashbuckle.AspNetCore 10.x's heavier initial render: - setUpPinnableFilterBar looked up .filter exactly once, at the same moment #swagger-ui's existence cleared the startup poll loop. Unlike #swagger-ui, .filter only renders after the OpenAPI document has been fetched and parsed, so it very often didn't exist yet at that point - the lookup would silently and permanently no-op, leaving the pin button missing "most of the time". Now polls for .filter with the same bounded-retry pattern setUpExpandAndCollapseOperationsButtons already uses. - Hiding Swashbuckle 10.x's native dark-mode toggle via CSS (previous commit) doesn't stop its React component from mounting: componentDidMount() still unconditionally adds a 'dark-mode' class to <html> based on prefers-color-scheme, regardless of the button's visibility. A MutationObserver now strips the class back off whenever it's added, since there's no reliable synchronous point to catch it once (the toggle component can mount before or after this script runs). Verified in-browser: pin-filterbar-btn present across repeated reloads; manually re-adding the dark-mode class gets stripped back off within ~150ms by the observer.
….txt Getting-Started's Troubleshooting section moved to its own wiki page (wiki commit a9ff0de) since it had grown long enough to bury the actual setup instructions above it. Surfaces the new page from the root README's top nav row and the DOCUMENTATION section of all four packages' readme.txt.
Each of the 4 package csproj files repeated the same Major/Minor/Patch numbers across separate Release- and Debug-conditioned property groups (Extensions doubly so, once per package, after the fix for Extensions inheriting Themes' Debug version). Versioning.props no longer hardcodes any package's version - it only computes the Debug/Release suffix and the final Version string generically from whatever Major/Minor/PatchVersion is already set. Each family now declares its version exactly once, unconditionally, in its own Versioning.Themes.props / Versioning.Extensions.props, imported by that family's csproj files before NuGet.props. Forgetting that import now leaves Major/Minor/PatchVersion blank (an obviously broken version, fails loud) instead of silently inheriting whichever family's props last set them. Also restores the Extensions packages' NuGet Description (API Counter specifically, not just "additional capabilities") and scopes the v3.x-breaking-changes warning in the README to Theme packages, since Extensions started fresh at v1.0 with nothing to migrate from. Verified: dotnet msbuild -t:GetVersion resolves 3.1.0/3.1.0-<rev> for both Theme packages and 1.0.0/1.0.0-<rev> for both Extensions packages in Release/Debug; full 354-test suite passes; dotnet pack produces correctly versioned nupkgs for both families.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
AspNetCore.SwaggerUI.Extensions,NSwag.AspNetCore.Extensions) with the API Counter feature (AppendOperationCountToTags()), previously duplicated inside the Themes packages - now removed from Themes and owned solely by Extensions.EnableFilter(pinnable: true)) and persists the pinnable topbar's pinned state across reloads.AspNetCore.SwaggerUI.Themes/NSwag.AspNetCore.Themesto 3.1.0; Extensions packages ship at 1.0.0.AdditionalSettingskey handling.Test plan
dotnet build -c Release- clean, 0 errorsdotnet test -c Release- 354/354 passing across net8.0/9.0/10.0