Skip to content

Extensions packages, pinnable filter bar, and v3.1 release prep - #41

Open
teociaps wants to merge 36 commits into
mainfrom
new-extensions
Open

Extensions packages, pinnable filter bar, and v3.1 release prep#41
teociaps wants to merge 36 commits into
mainfrom
new-extensions

Conversation

@teociaps

Copy link
Copy Markdown
Owner

Summary

  • Adds the new Extensions package family (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.
  • Adds pinnable filter bar (EnableFilter(pinnable: true)) and persists the pinnable topbar's pinned state across reloads.
  • Bumps AspNetCore.SwaggerUI.Themes / NSwag.AspNetCore.Themes to 3.1.0; Extensions packages ship at 1.0.0.
  • Restyles the Extensions packages' package-readme.md/readme.txt to match the Themes packages' format.
  • Various fixes: thread-safe theme/endpoint registries, per-app theme dispatch table, NSwag filter AdditionalSettings key handling.

Test plan

  • dotnet build -c Release - clean, 0 errors
  • dotnet test -c Release - 354/354 passing across net8.0/9.0/10.0

- 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
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.
…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.
…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.
@teociaps teociaps self-assigned this Aug 14, 2026
@teociaps teociaps added the enhancement New feature or request label Aug 14, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant