diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..51ed46c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,147 @@
+name: "?? Bug Report"
+description: "Report a bug or issue with SwaggerUI.Themes or SwaggerUI.Extensions"
+labels: ["bug"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thank you for reporting an issue! Please fill out the information below to help us investigate.
+
+ - type: dropdown
+ id: package_type
+ attributes:
+ label: "Package Type"
+ description: "Which type of package are you reporting an issue with?"
+ options:
+ - "Theme (AspNetCore.SwaggerUI.Themes)"
+ - "Theme (NSwag.AspNetCore.Themes)"
+ - "Extension (AspNetCore.SwaggerUI.Extensions)"
+ - "Extension (NSwag.AspNetCore.Extensions)"
+ - "Common (AspNetCore.Swagger.Themes.Common)"
+ - "Unsure"
+ validations:
+ required: true
+
+ - type: dropdown
+ id: integration
+ attributes:
+ label: "Integration Framework"
+ description: "Which Swagger integration are you using?"
+ options:
+ - "Swashbuckle.AspNetCore"
+ - "NSwag.AspNetCore"
+ - "Both"
+ - "Not applicable"
+ validations:
+ required: true
+
+ - type: input
+ id: dotnet_version
+ attributes:
+ label: ".NET Version"
+ description: "Which .NET version are you using? (e.g., 8.0, 9.0, 10.0)"
+ placeholder: "e.g., 8.0"
+ validations:
+ required: true
+
+ - type: input
+ id: package_version
+ attributes:
+ label: "Package Version"
+ description: "Which version of the package are you using?"
+ placeholder: "e.g., 3.0.0"
+ validations:
+ required: true
+
+ - type: textarea
+ id: description
+ attributes:
+ label: "Description"
+ description: "Clear and concise description of the bug"
+ placeholder: "What happened?"
+ validations:
+ required: true
+
+ - type: textarea
+ id: reproduction
+ attributes:
+ label: "Steps to Reproduce"
+ description: "Step-by-step instructions to reproduce the issue"
+ placeholder: |
+ 1. Configure the package with...
+ 2. Navigate to...
+ 3. See error...
+ validations:
+ required: true
+
+ - type: textarea
+ id: expected_behavior
+ attributes:
+ label: "Expected Behavior"
+ description: "What should happen?"
+ placeholder: "Describe the expected behavior"
+ validations:
+ required: true
+
+ - type: textarea
+ id: actual_behavior
+ attributes:
+ label: "Actual Behavior"
+ description: "What actually happens?"
+ placeholder: "Describe what actually happens"
+ validations:
+ required: true
+
+ - type: dropdown
+ id: browser
+ attributes:
+ label: "Browser (if UI-related)"
+ description: "Which browser are you using? (Only relevant for theme/UI issues)"
+ options:
+ - "Chrome"
+ - "Firefox"
+ - "Safari"
+ - "Edge"
+ - "Other"
+ - "Not applicable"
+
+ - type: textarea
+ id: code_snippet
+ attributes:
+ label: "Code Snippet"
+ description: "Relevant code that demonstrates the issue (optional)"
+ render: csharp
+ placeholder: |
+ // Your code here
+ app.UseSwaggerUI(Theme.Dark);
+
+ - type: textarea
+ id: error_logs
+ attributes:
+ label: "Error Logs or Stack Trace"
+ description: "Any relevant error messages or stack traces (optional)"
+ render: plain_text
+
+ - type: textarea
+ id: screenshots
+ attributes:
+ label: "Screenshots"
+ description: "Screenshots showing the issue (optional, for UI-related issues)"
+
+ - type: textarea
+ id: additional_context
+ attributes:
+ label: "Additional Context"
+ description: "Any other information that might be helpful"
+ placeholder: "Add any other context about the problem here"
+
+ - type: checkboxes
+ id: checklist
+ attributes:
+ label: "Verification"
+ options:
+ - label: "I have searched existing issues and found no duplicates"
+ required: true
+ - label: "I have provided clear reproduction steps"
+ required: true
+ - label: "I have included relevant error messages or logs"
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index a49eab2..7557f1f 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1 +1,8 @@
-blank_issues_enabled: true
\ No newline at end of file
+ο»Ώblank_issues_enabled: false
+contact_links:
+ - name: "β Ask a Question"
+ url: "https://github.com/teociaps/SwaggerUI.Themes/discussions"
+ about: "Have a question or want to discuss? Use GitHub Discussions!"
+ - name: "π View Documentation"
+ url: "https://github.com/teociaps/SwaggerUI.Themes/wiki"
+ about: "Check the wiki for comprehensive documentation and guides"
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..2cea18d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,105 @@
+name: "? Feature Request"
+description: "Suggest a new feature or enhancement for SwaggerUI.Themes or SwaggerUI.Extensions"
+labels: ["enhancement"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thank you for suggesting a feature! Please provide details to help us understand your idea.
+
+ - type: dropdown
+ id: feature_type
+ attributes:
+ label: "Feature Type"
+ description: "What category is your feature request for?"
+ options:
+ - "Theme (visual/styling enhancement)"
+ - "Extension (functional capability)"
+ - "Documentation"
+ - "Developer Experience"
+ - "Other"
+ validations:
+ required: true
+
+ - type: dropdown
+ id: package_scope
+ attributes:
+ label: "Package Scope"
+ description: "Which packages should this feature apply to?"
+ options:
+ - "Swashbuckle only (AspNetCore.SwaggerUI.*)"
+ - "NSwag only (NSwag.AspNetCore.*)"
+ - "Both Swashbuckle and NSwag"
+ - "Common/Shared"
+ - "Not sure"
+ validations:
+ required: true
+
+ - type: textarea
+ id: problem
+ attributes:
+ label: "Problem Statement"
+ description: "What problem does this feature solve? What is the use case?"
+ placeholder: |
+ Current situation: ...
+ Problem: ...
+ Why this matters: ...
+ validations:
+ required: true
+
+ - type: textarea
+ id: solution
+ attributes:
+ label: "Proposed Solution"
+ description: "Describe how you envision this feature working"
+ placeholder: |
+ Describe the ideal behavior/UI/API
+ validations:
+ required: true
+
+ - type: textarea
+ id: alternatives
+ attributes:
+ label: "Alternative Solutions"
+ description: "Are there any alternative approaches? (optional)"
+ placeholder: "Have you considered other ways to solve this?"
+
+ - type: textarea
+ id: example_usage
+ attributes:
+ label: "Example Usage"
+ description: "How would users interact with this feature? (optional)"
+ render: csharp
+ placeholder: |
+ // Example code showing how the feature would be used
+ app.UseSwaggerUI(Theme.Dark, c =>
+ {
+ c.MyNewFeature();
+ });
+
+ - type: textarea
+ id: design_considerations
+ attributes:
+ label: "Design Considerations"
+ description: "Any thoughts on API design, backward compatibility, or performance? (optional)"
+ placeholder: "Backward compatibility impact, performance considerations, naming suggestions, etc."
+
+ - type: checkboxes
+ id: checklist
+ attributes:
+ label: "Verification"
+ options:
+ - label: "I have searched existing feature requests and found no duplicates"
+ required: true
+ - label: "This feature aligns with the project scope"
+ required: true
+
+ - type: textarea
+ id: additional_context
+ attributes:
+ label: "Additional Context"
+ description: "Any other information or references? (optional)"
+ placeholder: |
+ - Related issues: #123
+ - Similar features in other projects: ...
+ - References: ...
diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml
new file mode 100644
index 0000000..ab86ca8
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/question.yml
@@ -0,0 +1,67 @@
+name: "? Question/Discussion"
+description: "Ask a question or start a discussion about using the packages"
+labels: ["question"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Have a question about how to use SwaggerUI.Themes or SwaggerUI.Extensions? Please ask away!
+
+ - type: dropdown
+ id: package_type
+ attributes:
+ label: "Package"
+ description: "Which package is your question about?"
+ options:
+ - "AspNetCore.SwaggerUI.Themes"
+ - "NSwag.AspNetCore.Themes"
+ - "AspNetCore.SwaggerUI.Extensions"
+ - "NSwag.AspNetCore.Extensions"
+ - "General/Not sure"
+ validations:
+ required: true
+
+ - type: input
+ id: dotnet_version
+ attributes:
+ label: ".NET Version"
+ description: "Which .NET version are you using?"
+ placeholder: "e.g., 8.0, 9.0, 10.0"
+
+ - type: input
+ id: package_version
+ attributes:
+ label: "Package Version"
+ description: "Which version of the package are you using?"
+ placeholder: "e.g., 3.0.0"
+
+ - type: textarea
+ id: question
+ attributes:
+ label: "Your Question"
+ description: "What would you like to know?"
+ placeholder: "Ask your question here..."
+ validations:
+ required: true
+
+ - type: textarea
+ id: context
+ attributes:
+ label: "Context"
+ description: "What are you trying to achieve? Include any relevant code."
+ render: csharp
+ placeholder: |
+ // Your code/setup here
+
+ - type: textarea
+ id: what_you_tried
+ attributes:
+ label: "What Have You Tried?"
+ description: "What solutions have you already attempted?"
+ placeholder: "Describe any troubleshooting steps you've already taken"
+
+ - type: textarea
+ id: additional_info
+ attributes:
+ label: "Additional Information"
+ description: "Any other details that might be helpful? (optional)"
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..70f60d7
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,65 @@
+## Description
+
+
+
+## Type of Change
+
+- [ ] ?? **Theme** (visual/styling updates)
+- [ ] ?? **Extension** (functional capability)
+- [ ] ?? **Documentation** (docs/comments only)
+- [ ] ?? **Bug Fix** (fixes an issue)
+- [ ] ?? **Refactoring** (internal improvements)
+- [ ] ?? **Tests** (test updates/additions)
+
+## Related Issue
+
+Fixes # or Relates to #
+
+## Package(s) Affected
+
+- [ ] AspNetCore.SwaggerUI.Themes
+- [ ] NSwag.AspNetCore.Themes
+- [ ] AspNetCore.SwaggerUI.Extensions
+- [ ] NSwag.AspNetCore.Extensions
+- [ ] AspNetCore.Swagger.Themes.Common
+
+## Testing
+
+Please describe the testing done to verify this change:
+
+
+
+- [ ] I have built the solution (`dotnet build -c Release`)
+- [ ] I have run the test suite (`dotnet test`)
+- [ ] I have tested with relevant sample projects
+- [ ] All changes work across .NET 8, 9, and 10 (if applicable)
+
+## Checklist
+
+- [ ] My code follows the style guidelines of this project
+- [ ] I have added/updated tests for my changes
+- [ ] I have updated relevant documentation (README, CONTRIBUTING, wiki, etc.)
+- [ ] My changes do not break any existing functionality
+- [ ] I have verified no new compiler warnings are introduced
+
+## Theme-Specific (if applicable)
+
+- [ ] Minified CSS/JS files have been updated
+- [ ] Minified file headers are correctly formatted (`/*Name*/...`)
+- [ ] All placeholders (e.g., `{$THEME_SWITCHER}`) are preserved in minified files
+- [ ] Theme is discoverable and testable
+
+## Extension-Specific (if applicable)
+
+- [ ] Implementation exists for both Swashbuckle and NSwag (if cross-platform)
+- [ ] Extension works independently and with other extensions
+- [ ] Tests cover the new functionality
+- [ ] Sample applications demonstrate the feature
+
+## Screenshots/Demo
+
+
+
+## Additional Context
+
+
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 3ca5a09..208032b 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -3,11 +3,11 @@ name: Build
on:
push:
branches: [main]
- tags: ['v*.*.*']
+ tags: ['themes-v*.*.*', 'extensions-v*.*.*']
paths-ignore: ['**/*.md', '**/*.png', '**/*.txt']
pull_request:
branches: [main]
- types: [opened, reopened, edited]
+ types: [opened, reopened, edited, synchronize]
paths-ignore: ['**/*.md', '**/*.png', '**/*.txt']
env:
diff --git a/.github/workflows/pack_and_deploy.yml b/.github/workflows/pack_and_deploy.yml
index 497f5ef..38d555f 100644
--- a/.github/workflows/pack_and_deploy.yml
+++ b/.github/workflows/pack_and_deploy.yml
@@ -13,6 +13,9 @@ env:
jobs:
deploy:
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ id-token: write # required so GitHub can mint the OIDC token NuGet Trusted Publishing exchanges for a short-lived API key
steps:
- name: Checkout repository
@@ -31,6 +34,27 @@ jobs:
9.0.x
10.0.x
+ - name: Determine package family from release tag
+ id: family
+ shell: bash
+ env:
+ TAG: ${{ github.event.release.tag_name }}
+ run: |
+ if [[ "$TAG" == themes-v* ]]; then
+ echo "filter=Themes" >> "$GITHUB_OUTPUT"
+ elif [[ "$TAG" == extensions-v* ]]; then
+ echo "filter=Extensions" >> "$GITHUB_OUTPUT"
+ else
+ echo "::error::Release tag '$TAG' does not match themes-v* or extensions-v* β cannot determine package family to deploy." >&2
+ exit 1
+ fi
+
+ - name: NuGet login (OIDC β temp API key)
+ uses: NuGet/login@v1
+ id: nuget_login
+ with:
+ user: ${{ secrets.NUGET_USER }} # nuget.org username (profile name), NOT the email address
+
- name: Pack & Push to NuGet
shell: pwsh
- run: ./deploy.ps1 -Configuration ${{ env.CONFIGURATION }} -PackageOutput ${{ env.PACKAGE_OUTPUT }} -NuGetSourceUrl ${{ env.NUGET_SOURCE_URL }} -NuGetApiKey ${{ secrets.NUGET_API_KEY }}
\ No newline at end of file
+ run: ./deploy.ps1 -Configuration ${{ env.CONFIGURATION }} -PackageOutput ${{ env.PACKAGE_OUTPUT }} -NuGetSourceUrl ${{ env.NUGET_SOURCE_URL }} -NuGetApiKey ${{ steps.nuget_login.outputs.NUGET_API_KEY }} -Filter ${{ steps.family.outputs.filter }}
\ No newline at end of file
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index cb9c05f..e23841d 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -7,7 +7,7 @@ on:
- completed
pull_request:
branches: [main]
- types: [opened, reopened, edited]
+ types: [opened, reopened, edited, synchronize]
paths-ignore: ['**/*.md', '**/*.png', '**/*.txt']
env:
@@ -31,4 +31,8 @@ jobs:
10.0.x
- name: Test
- run: dotnet test ${{ env.TEST_PROJECTS_PATH }} --configuration ${{ env.CONFIGURATION }}
\ No newline at end of file
+ shell: bash
+ run: |
+ for project in ${{ env.TEST_PROJECTS_PATH }}; do
+ dotnet test "$project" --configuration ${{ env.CONFIGURATION }}
+ done
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0380fa7..0f78073 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -35,12 +35,18 @@ This project and everyone participating in it are governed by our [Code of Condu
Understanding the project layout helps you navigate the codebase:
- **`src/AspNetCore.Swagger.Themes.Common/`** - Core theme system shared between packages
-- **`src/AspNetCore.SwaggerUI.Themes/`** - Swashbuckle.AspNetCore integration
-- **`src/NSwag.AspNetCore.Themes/`** - NSwag.AspNetCore integration
+- **`src/AspNetCore.SwaggerUI.Themes/`** - Swashbuckle.AspNetCore theme integration
+- **`src/NSwag.AspNetCore.Themes/`** - NSwag.AspNetCore theme integration
+- **`src/AspNetCore.SwaggerUI.Extensions/`** - Swashbuckle.AspNetCore functional extensions
+- **`src/NSwag.AspNetCore.Extensions/`** - NSwag.AspNetCore functional extensions
- **`samples/`** - Example applications demonstrating usage
- **`tests/`** - Unit and integration tests
-## Working with Themes
+---
+
+## Contributing Themes
+
+Themes customize the visual appearance of Swagger UI. When contributing a new theme:
### Adding or Modifying Themes
@@ -107,6 +113,82 @@ git add src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Scripts/*
git commit -m "Add new ocean theme"
```
+### Theme Organization
+
+When adding example custom themes, use subfolders for clarity:
+
+```
+samples/YourSample/SwaggerThemes/
+βββ CompanyThemes/
+β βββ corporate-blue.css
+βββ SeasonalThemes/
+ βββ holiday-red.css
+```
+
+This demonstrates best practices for users organizing their own themes.
+
+### Standalone vs. Regular Themes
+
+- **Regular themes** - Depend on `common.css` and may use JavaScript features
+- **Standalone themes** - Prefix with `standalone.` (e.g., `standalone.custom.css`) for zero dependencies
+
+Choose based on whether you need shared styles or prefer total independence.
+
+---
+
+## Contributing Extensions
+
+Extensions add functional capabilities to Swagger UI beyond theming. When contributing a new extension:
+
+### Planning an Extension
+
+Before implementing, consider:
+
+1. **Scope** - Is it a focused feature or part of a larger system?
+2. **Compatibility** - Does it work with both Swashbuckle and NSwag?
+3. **Integration points** - Where should it hook into the pipeline?
+4. **User value** - What problem does it solve for users?
+
+### Implementation Guidelines
+
+Extensions should be implemented for both integrations:
+
+- **Swashbuckle implementation** - `src/AspNetCore.SwaggerUI.Extensions/`
+ - Add service extension methods in `Microsoft.Extensions.DependencyInjection/`
+ - Add filters in `AspNetCore/Swagger/Extensions/Filters/`
+
+- **NSwag implementation** - `src/NSwag.AspNetCore.Extensions/`
+ - Add service extension methods in `Microsoft.Extensions.DependencyInjection/`
+ - Add processors in `AspNetCore/Swagger/Extensions/Processors/`
+
+### Extension Structure Example
+
+```
+src/AspNetCore.SwaggerUI.Extensions/
+βββ Microsoft/Extensions/DependencyInjection/
+β βββ SwaggerGenOptionsExtensions.cs // Service configuration
+βββ AspNetCore/Swagger/Extensions/
+β βββ Filters/
+β βββ MyFeatureFilter.cs // Swagger/OpenAPI filter
+βββ package-readme.md // NuGet package documentation
+
+src/NSwag.AspNetCore.Extensions/
+βββ Microsoft/Extensions/DependencyInjection/
+β βββ AspNetCoreOpenApiDocumentGeneratorSettingsExtensions.cs
+βββ AspNetCore/Swagger/Extensions/
+β βββ Processors/
+β βββ MyFeatureProcessor.cs // NSwag processor
+βββ package-readme.md
+```
+
+### Testing Extensions
+
+1. Add tests in the appropriate test project
+2. Test with both sample applications
+3. Verify the extension works independently and alongside other extensions
+
+---
+
## Testing Your Changes
Before submitting a pull request:
@@ -126,8 +208,9 @@ dotnet test
```
All tests must pass, including:
-- Minified header format tests
-- Theme discovery tests
+- Minified header format tests (for themes)
+- Theme discovery tests (for themes)
+- Extension functionality tests (for extensions)
- Integration tests
### 3. Test with Sample Applications
@@ -140,15 +223,22 @@ dotnet run --project samples/Sample.AspNetCore.SwaggerUI.Swashbuckle
# NSwag sample
dotnet run --project samples/Sample.AspNetCore.SwaggerUI.NSwag
+
+# Swashbuckle Extensions sample
+dotnet run --project samples/Sample.AspNetCore.SwaggerUI.Extensions
+
+# NSwag Extensions sample
+dotnet run --project samples/Sample.NSwag.AspNetCore.Extensions
```
### 4. Manual Verification
Open the sample app in your browser and verify:
-- β
Themes render correctly
-- β
Theme switcher dropdown appears and works (if enabled)
-- β
Custom themes are discovered automatically
+- β
Themes render correctly (if theme changes)
+- β
Theme switcher dropdown appears and works (if theme changes)
+- β
Custom themes are discovered automatically (if theme changes)
+- β
Extensions work as expected (if extension changes)
- β
Advanced features work (pinnable topbar, back-to-top, etc.)
- β
No console errors in browser developer tools
- β
Standalone themes load without extra dependencies
@@ -164,9 +254,10 @@ Open the sample app in your browser and verify:
### Good PR Practices
-- Use descriptive titles: `feat: add sunset theme` not `update css`
+- Use descriptive titles: `feat: add sunset theme` or `feat: add api metadata extension`
- Reference issues: `Fixes #123` or `Closes #456`
-- Include screenshots for visual changes
+- Include screenshots for visual changes (themes)
+- Include before/after examples for functional changes (extensions)
- Test across .NET 8, 9, and 10 if possible
## Reporting Bugs
@@ -177,20 +268,26 @@ Found a bug? Please open an issue with:
- **Steps to reproduce** the issue
- **Expected vs. actual behavior**
- **Environment details:**
- - Which package (Swashbuckle or NSwag)
+ - Which package (Themes or Extensions)
+ - Which integration (Swashbuckle or NSwag)
- .NET version
- Browser (if UI-related)
- **Screenshots or code snippets** if applicable
+See the bug report template for a structured approach.
+
## Suggesting Features
We welcome feature suggestions! When proposing a feature:
- **Describe the use case** - What problem does it solve?
+- **Specify the scope** - Is it for Themes, Extensions, or both?
- **Explain the benefit** - How does it improve the project?
- **Consider backward compatibility** - Will it break existing code?
- **Offer to help** - Can you contribute the implementation?
+See the feature request template for a structured approach.
+
## Development Tips
### Quick Testing Loop
@@ -216,13 +313,6 @@ samples/YourSample/SwaggerThemes/
This demonstrates best practices for users organizing their own themes.
-### Standalone vs. Regular Themes
-
-- **Regular themes** - Depend on `common.css` and may use JavaScript features
-- **Standalone themes** - Prefix with `standalone.` (e.g., `standalone.custom.css`) for zero dependencies
-
-Choose based on whether you need shared styles or prefer total independence.
-
## Questions?
Not sure about something? Feel free to:
diff --git a/README.md b/README.md
index aad1d79..cee55a3 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@
_**...and more!**_
-**[Get Started](https://github.com/teociaps/SwaggerUI.Themes/wiki/Getting-Started)** β’ **[View Built-in Themes](https://github.com/teociaps/SwaggerUI.Themes/wiki/Predefined-Themes)** β’ **[Full Documentation](https://github.com/teociaps/SwaggerUI.Themes/wiki)**
+**[Get Started](https://github.com/teociaps/SwaggerUI.Themes/wiki/Getting-Started)** β’ **[View Built-in Themes](https://github.com/teociaps/SwaggerUI.Themes/wiki/Predefined-Themes)** β’ **[Troubleshooting](https://github.com/teociaps/SwaggerUI.Themes/wiki/Troubleshooting)** β’ **[Full Documentation](https://github.com/teociaps/SwaggerUI.Themes/wiki)**
@@ -29,17 +29,30 @@ _Note: GIF framerate is limited. Actual transitions are buttery smooth!_
---
+## π¦ Available Packages
+
+### Theme Packages
+Customize the look and feel of your Swagger UI with runtime theme switching and custom theme creation.
+
| Package | Purpose | NuGet |
|---------|---------|-------|
| **AspNetCore.SwaggerUI.Themes** | For [Swashbuckle.AspNetCore][swashbuckle-link] | [![swashbuckle-nuget]][swashbuckle-nuget-link] |
| **NSwag.AspNetCore.Themes** | For [NSwag.AspNetCore][nswag-link] | [![nswag-nuget]][nswag-nuget-link] |
+### Extension Packages
+Enhance your Swagger UI with additional functional capabilities beyond theming.
+
+| Package | Purpose | NuGet |
+|---------|---------|-------|
+| **AspNetCore.SwaggerUI.Extensions** | Functional enhancements for [Swashbuckle.AspNetCore][swashbuckle-link] | [![swashbuckle-ext-nuget]][swashbuckle-ext-nuget-link] |
+| **NSwag.AspNetCore.Extensions** | Functional enhancements for [NSwag.AspNetCore][nswag-link] | [![nswag-ext-nuget]][nswag-ext-nuget-link] |
+
---
> [!WARNING]
-> **Version 3.x Breaking Changes**
+> **Version 3.x Breaking Changes** (Theme packages only)
>
> Upgrading from v2.x? Please review the **[Migration Guide](https://github.com/teociaps/SwaggerUI.Themes/wiki/Migration-v3)** for important API changes.
@@ -47,12 +60,19 @@ _Note: GIF framerate is limited. Actual transitions are buttery smooth!_
## π Quick Start
```bash
-# Install package
+# Install theme package
dotnet add package AspNetCore.SwaggerUI.Themes
# or
dotnet add package NSwag.AspNetCore.Themes
+
+# Or install extensions package
+dotnet add package AspNetCore.SwaggerUI.Extensions
+# or
+dotnet add package NSwag.AspNetCore.Extensions
```
+### Themes Quick Start
+
```csharp
// Apply a theme - that's it!
app.UseSwaggerUI(Theme.Dark); // Swashbuckle
@@ -61,10 +81,31 @@ app.UseSwaggerUi(Theme.Dark); // NSwag
// Enable runtime theme switcher!
app.UseSwaggerUI(Theme.Dark, c => c.EnableThemeSwitcher());
+
+// Enable a pinnable filter box
+app.UseSwaggerUI(Theme.Dark, c => c.EnableFilter(pinnable: true));
+```
+
+### Extensions Quick Start
+
+```csharp
+// Add extensions to your configuration
+builder.Services.AddSwaggerGen(c =>
+{
+ c.AppendOperationCountToTags(); // Swashbuckle
+});
+
+// or for NSwag
+builder.Services.AddOpenApiDocument(c =>
+{
+ c.AppendOperationCountToTags(); // NSwag
+});
```
+
## β¨ Features
+### Themes
- π₯ **[Theme Switcher](https://github.com/teociaps/SwaggerUI.Themes/wiki/Feature-Dynamic-Theme-Switcher)** - Switch built-in and custom themes dynamically without page reload
- **[Built-in Themes](https://github.com/teociaps/SwaggerUI.Themes/wiki/Predefined-Themes)** - Choose from predefined themes ready to use
@@ -73,11 +114,14 @@ app.UseSwaggerUI(Theme.Dark, c => c.EnableThemeSwitcher());
- **[Advanced Features](https://github.com/teociaps/SwaggerUI.Themes/wiki/Advanced-Options)** - Enhance your documentation with powerful UI capabilities
-- _...discover more in the [Wiki](https://github.com/teociaps/SwaggerUI.Themes/wiki/Features)!_
+### Extensions
+- **API Counter** - Automatically display operation counts in tag descriptions for better API surface area visibility
+
+_Discover more features in the [Wiki](https://github.com/teociaps/SwaggerUI.Themes/wiki/Features)!_
## π Basic Usage Examples
-### Swashbuckle
+### Swashbuckle Themes
```csharp
using AspNetCore.Swagger.Themes;
@@ -102,7 +146,7 @@ app.UseSwaggerUI(Theme.Dark, c =>
...
```
-### NSwag
+### NSwag Themes
```csharp
using AspNetCore.Swagger.Themes;
@@ -127,6 +171,44 @@ app.UseSwaggerUi(Theme.Dark, c =>
...
```
+### Swashbuckle Extensions
+
+```csharp
+using AspNetCore.Swagger.Extensions;
+
+...
+
+builder.Services.AddSwaggerGen(c =>
+{
+ // Add operation count to tag descriptions
+ c.AppendOperationCountToTags();
+
+ // Or with custom template
+ c.AppendOperationCountToTags(" [{0} endpoints]");
+});
+
+...
+```
+
+### NSwag Extensions
+
+```csharp
+using AspNetCore.Swagger.Extensions;
+
+...
+
+builder.Services.AddOpenApiDocument(c =>
+{
+ // Add operation count to tag descriptions
+ c.AppendOperationCountToTags();
+
+ // Or with custom template
+ c.AppendOperationCountToTags(" [{0} endpoints]");
+});
+
+...
+```
+
### π‘ Build your own Custom Theme
```csharp
@@ -151,7 +233,7 @@ _Learn advanced usages in the [Wiki](https://github.com/teociaps/SwaggerUI.Theme
## π€ Contributing
-Contributions are welcome! See the [Contributing Guide](CONTRIBUTING.md) for details.
+Contributions are welcome! See the [Contributing Guide](CONTRIBUTING.md) for details on contributing themes and extensions.
## π License
@@ -168,7 +250,11 @@ MIT Licensed - see [LICENSE](LICENSE) for details.
[swashbuckle-nuget]: https://img.shields.io/nuget/v/AspNetCore.SwaggerUI.Themes?logo=nuget&label=Version&color=blue
[swashbuckle-nuget-link]: https://www.nuget.org/packages/AspNetCore.SwaggerUI.Themes/
+[swashbuckle-ext-nuget]: https://img.shields.io/nuget/v/AspNetCore.SwaggerUI.Extensions?logo=nuget&label=Version&color=blue
+[swashbuckle-ext-nuget-link]: https://www.nuget.org/packages/AspNetCore.SwaggerUI.Extensions/
[nswag-nuget]: https://img.shields.io/nuget/v/NSwag.AspNetCore.Themes?logo=nuget&label=Version&color=blue
[nswag-nuget-link]: https://www.nuget.org/packages/NSwag.AspNetCore.Themes/
+[nswag-ext-nuget]: https://img.shields.io/nuget/v/NSwag.AspNetCore.Extensions?logo=nuget&label=Version&color=blue
+[nswag-ext-nuget-link]: https://www.nuget.org/packages/NSwag.AspNetCore.Extensions/
[swashbuckle-link]: https://github.com/domaindrivendev/Swashbuckle.AspNetCore
[nswag-link]: https://github.com/RicoSuter/NSwag
\ No newline at end of file
diff --git a/SwaggerUI.Themes.sln b/SwaggerUI.Themes.sln
index fc8d08a..c94ef4c 100644
--- a/SwaggerUI.Themes.sln
+++ b/SwaggerUI.Themes.sln
@@ -1,7 +1,7 @@
ο»Ώ
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
-VisualStudioVersion = 18.0.11205.157 d18.0
+VisualStudioVersion = 18.0.11205.157
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCore.SwaggerUI.Themes", "src\AspNetCore.SwaggerUI.Themes\AspNetCore.SwaggerUI.Themes.csproj", "{5FC2CEDA-5475-4096-BDEC-1C9BFC933FB8}"
EndProject
@@ -29,6 +29,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NSwag.AspNetCore.Themes", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCore.Swagger.Themes.Common", "src\AspNetCore.Swagger.Themes.Common\AspNetCore.Swagger.Themes.Common.csproj", "{DCD47D0E-ADD5-4B6A-BDAA-DD6559EB384D}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore.SwaggerUI.Extensions", "src\AspNetCore.SwaggerUI.Extensions\AspNetCore.SwaggerUI.Extensions.csproj", "{8E498417-7FF6-E0E3-9BC1-AB5197733AFB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NSwag.AspNetCore.Extensions", "src\NSwag.AspNetCore.Extensions\NSwag.AspNetCore.Extensions.csproj", "{605058F0-1D5E-C122-1ABD-B73D053665B7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore.Swagger.Extensions.Tests", "tests\AspNetCore.Swagger.Extensions.Tests\AspNetCore.Swagger.Extensions.Tests.csproj", "{ECD67BCE-D96A-B1D4-48F1-95AF9F6A49E1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.AspNetCore.SwaggerUI.Extensions", "samples\Sample.AspNetCore.SwaggerUI.Extensions\Sample.AspNetCore.SwaggerUI.Extensions.csproj", "{B90ED39A-C413-8F86-152F-7D2EB7AAB349}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.NSwag.AspNetCore.Extensions", "samples\Sample.NSwag.AspNetCore.Extensions\Sample.NSwag.AspNetCore.Extensions.csproj", "{9050F5CA-0F00-25EA-236F-8659C3AD917F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -59,6 +69,26 @@ Global
{DCD47D0E-ADD5-4B6A-BDAA-DD6559EB384D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DCD47D0E-ADD5-4B6A-BDAA-DD6559EB384D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DCD47D0E-ADD5-4B6A-BDAA-DD6559EB384D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8E498417-7FF6-E0E3-9BC1-AB5197733AFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8E498417-7FF6-E0E3-9BC1-AB5197733AFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8E498417-7FF6-E0E3-9BC1-AB5197733AFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8E498417-7FF6-E0E3-9BC1-AB5197733AFB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {605058F0-1D5E-C122-1ABD-B73D053665B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {605058F0-1D5E-C122-1ABD-B73D053665B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {605058F0-1D5E-C122-1ABD-B73D053665B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {605058F0-1D5E-C122-1ABD-B73D053665B7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {ECD67BCE-D96A-B1D4-48F1-95AF9F6A49E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {ECD67BCE-D96A-B1D4-48F1-95AF9F6A49E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {ECD67BCE-D96A-B1D4-48F1-95AF9F6A49E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {ECD67BCE-D96A-B1D4-48F1-95AF9F6A49E1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B90ED39A-C413-8F86-152F-7D2EB7AAB349}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B90ED39A-C413-8F86-152F-7D2EB7AAB349}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B90ED39A-C413-8F86-152F-7D2EB7AAB349}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B90ED39A-C413-8F86-152F-7D2EB7AAB349}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9050F5CA-0F00-25EA-236F-8659C3AD917F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9050F5CA-0F00-25EA-236F-8659C3AD917F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9050F5CA-0F00-25EA-236F-8659C3AD917F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9050F5CA-0F00-25EA-236F-8659C3AD917F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -70,6 +100,11 @@ Global
{E8C376AC-9933-4ECF-92D8-66BC706FD6D0} = {FF08B180-6923-46E0-8426-99391CFB47CD}
{38614F76-C962-4CDC-9C9D-25469B92CDE2} = {19896985-FCB0-4EA0-8E5F-FF3902AC13FC}
{DCD47D0E-ADD5-4B6A-BDAA-DD6559EB384D} = {19896985-FCB0-4EA0-8E5F-FF3902AC13FC}
+ {8E498417-7FF6-E0E3-9BC1-AB5197733AFB} = {19896985-FCB0-4EA0-8E5F-FF3902AC13FC}
+ {605058F0-1D5E-C122-1ABD-B73D053665B7} = {19896985-FCB0-4EA0-8E5F-FF3902AC13FC}
+ {ECD67BCE-D96A-B1D4-48F1-95AF9F6A49E1} = {B89F6FD2-68B6-4A70-A070-FF22FE42DDE4}
+ {B90ED39A-C413-8F86-152F-7D2EB7AAB349} = {FF08B180-6923-46E0-8426-99391CFB47CD}
+ {9050F5CA-0F00-25EA-236F-8659C3AD917F} = {FF08B180-6923-46E0-8426-99391CFB47CD}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {660A434B-79DE-4C3F-892C-333338978C59}
diff --git a/assets/pinnable-filter-bar.gif b/assets/pinnable-filter-bar.gif
new file mode 100644
index 0000000..2c1dadc
Binary files /dev/null and b/assets/pinnable-filter-bar.gif differ
diff --git a/build/Versioning.Extensions.props b/build/Versioning.Extensions.props
new file mode 100644
index 0000000..6c7c96e
--- /dev/null
+++ b/build/Versioning.Extensions.props
@@ -0,0 +1,12 @@
+
+
+
+
+ 1
+ 0
+ 0
+
+
+
diff --git a/build/Versioning.Themes.props b/build/Versioning.Themes.props
new file mode 100644
index 0000000..cdb5aed
--- /dev/null
+++ b/build/Versioning.Themes.props
@@ -0,0 +1,12 @@
+
+
+
+
+ 3
+ 1
+ 0
+
+
+
diff --git a/build/Versioning.props b/build/Versioning.props
index d078017..2c30a01 100644
--- a/build/Versioning.props
+++ b/build/Versioning.props
@@ -1,6 +1,15 @@
-
+
+
+
true
$([System.DateTime]::Now.Ticks)
@@ -10,10 +19,15 @@
$([System.Math]::Floor($(MinutesSinceMidnight)))
$([System.DateTime]::Today.ToString("yyMMdd"))
$(Today)-$(Floored)
- 3
- 0
- 2
-$(Revision)
+
+
+
+
+
+
+
+
$(MajorVersion).$(MinorVersion).$(PatchVersion)$(VersionSuffix)
@@ -24,4 +38,4 @@
-
\ No newline at end of file
+
diff --git a/deploy.ps1 b/deploy.ps1
index 001711a..43d19a2 100644
--- a/deploy.ps1
+++ b/deploy.ps1
@@ -3,16 +3,23 @@ param (
[string]$Configuration = "Release",
[string]$PackageOutput = "$(pwd)/artifacts",
[string]$NuGetSourceUrl = "https://api.nuget.org/v3/index.json",
- [string]$NuGetApiKey
+ [string]$NuGetApiKey,
+ [string]$Filter
)
+# Validate the package family filter (case-sensitive: "themes" must not silently
+# slip through and produce a glob that matches nothing on case-sensitive filesystems)
+if ($Filter -cne "Themes" -and $Filter -cne "Extensions") {
+ throw "Invalid -Filter value '${Filter}'. Must be 'Themes' or 'Extensions'."
+}
+
# Ensure the output directory exists
if (-not (Test-Path -Path $PackageOutput)) {
New-Item -ItemType Directory -Path $PackageOutput | Out-Null
}
# Iterate over each project to pack
-Get-ChildItem -Path src -Filter *.Themes -Directory | ForEach-Object {
+Get-ChildItem -Path src -Filter "*.$Filter" -Directory | ForEach-Object {
$projectPath = $_.FullName
$projectName = $_.Name
$csprojPath = Join-Path -Path $projectPath -ChildPath "${projectName}.csproj"
diff --git a/samples/Sample.AspNetCore.SwaggerUI.Extensions/Controllers/OrdersController.cs b/samples/Sample.AspNetCore.SwaggerUI.Extensions/Controllers/OrdersController.cs
new file mode 100644
index 0000000..8db5b48
--- /dev/null
+++ b/samples/Sample.AspNetCore.SwaggerUI.Extensions/Controllers/OrdersController.cs
@@ -0,0 +1,27 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+[Tags("Orders")]
+public class OrdersController : ControllerBase
+{
+ ///
+ /// Gets all orders
+ ///
+ [HttpGet]
+ public IActionResult GetAll()
+ {
+ return Ok(new[] { new { Id = 1, Total = 150.00 }, new { Id = 2, Total = 200.00 } });
+ }
+
+ ///
+ /// Gets an order by ID
+ ///
+ [HttpGet("{id}")]
+ public IActionResult GetById(int id)
+ {
+ return Ok(new { Id = id, Total = 150.00, Status = "Pending" });
+ }
+}
\ No newline at end of file
diff --git a/samples/Sample.AspNetCore.SwaggerUI.Extensions/Controllers/ProductsController.cs b/samples/Sample.AspNetCore.SwaggerUI.Extensions/Controllers/ProductsController.cs
new file mode 100644
index 0000000..ef7b567
--- /dev/null
+++ b/samples/Sample.AspNetCore.SwaggerUI.Extensions/Controllers/ProductsController.cs
@@ -0,0 +1,36 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+[Tags("Products")]
+public class ProductsController : ControllerBase
+{
+ ///
+ /// Gets all products
+ ///
+ [HttpGet]
+ public IActionResult GetAll()
+ {
+ return Ok(new[] { new { Id = 1, Name = "Product A" }, new { Id = 2, Name = "Product B" } });
+ }
+
+ ///
+ /// Gets a product by ID
+ ///
+ [HttpGet("{id}")]
+ public IActionResult GetById(int id)
+ {
+ return Ok(new { Id = id, Name = "Product", Price = 99.99 });
+ }
+
+ ///
+ /// Creates a new product
+ ///
+ [HttpPost]
+ public IActionResult Create([FromBody] object product)
+ {
+ return CreatedAtAction(nameof(GetById), new { id = 1 }, product);
+ }
+}
\ No newline at end of file
diff --git a/samples/Sample.AspNetCore.SwaggerUI.Extensions/Controllers/UsersController.cs b/samples/Sample.AspNetCore.SwaggerUI.Extensions/Controllers/UsersController.cs
new file mode 100644
index 0000000..68160dd
--- /dev/null
+++ b/samples/Sample.AspNetCore.SwaggerUI.Extensions/Controllers/UsersController.cs
@@ -0,0 +1,54 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+[Tags("Users")]
+public class UsersController : ControllerBase
+{
+ ///
+ /// Gets all users
+ ///
+ [HttpGet]
+ public IActionResult GetAll()
+ {
+ return Ok(new[] { new { Id = 1, Name = "John" }, new { Id = 2, Name = "Jane" } });
+ }
+
+ ///
+ /// Gets a user by ID
+ ///
+ [HttpGet("{id}")]
+ public IActionResult GetById(int id)
+ {
+ return Ok(new { Id = id, Name = "John Doe" });
+ }
+
+ ///
+ /// Creates a new user
+ ///
+ [HttpPost]
+ public IActionResult Create([FromBody] object user)
+ {
+ return CreatedAtAction(nameof(GetById), new { id = 1 }, user);
+ }
+
+ ///
+ /// Updates an existing user
+ ///
+ [HttpPut("{id}")]
+ public IActionResult Update(int id, [FromBody] object user)
+ {
+ return NoContent();
+ }
+
+ ///
+ /// Deletes a user
+ ///
+ [HttpDelete("{id}")]
+ public IActionResult Delete(int id)
+ {
+ return NoContent();
+ }
+}
\ No newline at end of file
diff --git a/samples/Sample.AspNetCore.SwaggerUI.Extensions/Program.cs b/samples/Sample.AspNetCore.SwaggerUI.Extensions/Program.cs
new file mode 100644
index 0000000..dcaf26b
--- /dev/null
+++ b/samples/Sample.AspNetCore.SwaggerUI.Extensions/Program.cs
@@ -0,0 +1,29 @@
+var builder = WebApplication.CreateBuilder(args);
+builder.Services.AddControllers();
+
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen(c =>
+{
+ c.SwaggerDoc("v1", new() { Title = "Sample API - Extensions Demo", Version = "v1" });
+
+ // API Counter Extension
+ // Automatically appends operation count to tag descriptions
+ c.AppendOperationCountToTags();
+
+ // Or with custom template:
+ // c.AppendOperationCountToTags(" [{0} endpoints]");
+ // c.AppendOperationCountToTags(" (APIs: {0})");
+});
+
+var app = builder.Build();
+
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseHttpsRedirection();
+app.MapControllers();
+
+await app.RunAsync();
\ No newline at end of file
diff --git a/samples/Sample.AspNetCore.SwaggerUI.Extensions/Properties/launchSettings.json b/samples/Sample.AspNetCore.SwaggerUI.Extensions/Properties/launchSettings.json
new file mode 100644
index 0000000..d0318af
--- /dev/null
+++ b/samples/Sample.AspNetCore.SwaggerUI.Extensions/Properties/launchSettings.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5100",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7100;http://localhost:5100",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/samples/Sample.AspNetCore.SwaggerUI.Extensions/Sample.AspNetCore.SwaggerUI.Extensions.csproj b/samples/Sample.AspNetCore.SwaggerUI.Extensions/Sample.AspNetCore.SwaggerUI.Extensions.csproj
new file mode 100644
index 0000000..aec7bfb
--- /dev/null
+++ b/samples/Sample.AspNetCore.SwaggerUI.Extensions/Sample.AspNetCore.SwaggerUI.Extensions.csproj
@@ -0,0 +1,28 @@
+
+
+
+
+
+ true
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/Sample.AspNetCore.SwaggerUI.Extensions/appsettings.Development.json b/samples/Sample.AspNetCore.SwaggerUI.Extensions/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/samples/Sample.AspNetCore.SwaggerUI.Extensions/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/samples/Sample.AspNetCore.SwaggerUI.Extensions/appsettings.json b/samples/Sample.AspNetCore.SwaggerUI.Extensions/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/samples/Sample.AspNetCore.SwaggerUI.Extensions/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/samples/Sample.AspNetCore.SwaggerUI.NSwag/Program.cs b/samples/Sample.AspNetCore.SwaggerUI.NSwag/Program.cs
index 503ef0b..5560742 100644
--- a/samples/Sample.AspNetCore.SwaggerUI.NSwag/Program.cs
+++ b/samples/Sample.AspNetCore.SwaggerUI.NSwag/Program.cs
@@ -5,7 +5,10 @@
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
-builder.Services.AddOpenApiDocument(OpenApiDocGenConfigurer.Configure);
+builder.Services.AddOpenApiDocument(c =>
+{
+ OpenApiDocGenConfigurer.Configure(c);
+});
var app = builder.Build();
@@ -126,11 +129,11 @@
// ========================================
// 13. Enable all advanced UI features
- app.UseSwaggerUi(Theme.Dark, c =>
- {
- c.DocumentTitle = "Sample API - All Features";
- c.EnableAllAdvancedOptions();
- });
+ //app.UseSwaggerUi(Theme.Dark, c =>
+ //{
+ // c.DocumentTitle = "Sample API - All Features";
+ // c.EnableAllAdvancedOptions();
+ //});
// 14. Individual advanced features
//app.UseSwaggerUi(Theme.Light, c =>
@@ -187,6 +190,14 @@
// .WithThemes(Theme.Dark, Theme.Light)
// .WithCustomThemes(CustomThemeMode.None));
//});
+
+ // 19. Pinnable filter bar
+ app.UseSwaggerUi(Theme.Dark, c =>
+ {
+ c.DocumentTitle = "Sample API - Pinnable Filter Bar";
+ c.EnableAllAdvancedOptions();
+ c.EnableFilter(pinnable: true);
+ });
}
app.UseHttpsRedirection();
diff --git a/samples/Sample.AspNetCore.SwaggerUI.NSwag/Sample.AspNetCore.SwaggerUI.NSwag.csproj b/samples/Sample.AspNetCore.SwaggerUI.NSwag/Sample.AspNetCore.SwaggerUI.NSwag.csproj
index ef4ed4a..3e3a122 100644
--- a/samples/Sample.AspNetCore.SwaggerUI.NSwag/Sample.AspNetCore.SwaggerUI.NSwag.csproj
+++ b/samples/Sample.AspNetCore.SwaggerUI.NSwag/Sample.AspNetCore.SwaggerUI.NSwag.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/samples/Sample.AspNetCore.SwaggerUI.Swashbuckle/Program.cs b/samples/Sample.AspNetCore.SwaggerUI.Swashbuckle/Program.cs
index e9974d9..25489c7 100644
--- a/samples/Sample.AspNetCore.SwaggerUI.Swashbuckle/Program.cs
+++ b/samples/Sample.AspNetCore.SwaggerUI.Swashbuckle/Program.cs
@@ -5,7 +5,10 @@
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
-builder.Services.AddSwaggerGen(SwaggerGenConfigurer.Configure);
+builder.Services.AddSwaggerGen(c =>
+{
+ SwaggerGenConfigurer.Configure(c);
+});
var app = builder.Build();
@@ -126,11 +129,11 @@
// ========================================
// 13. Enable all advanced UI features
- app.UseSwaggerUI(Theme.Dark, c =>
- {
- c.DocumentTitle = "Sample API - All Features";
- c.EnableAllAdvancedOptions();
- });
+ //app.UseSwaggerUI(Theme.Dark, c =>
+ //{
+ // c.DocumentTitle = "Sample API - All Features";
+ // c.EnableAllAdvancedOptions();
+ //});
// 14. Individual advanced features
//app.UseSwaggerUI(Theme.Light, c =>
@@ -188,6 +191,14 @@
// .WithThemes(Theme.Dark, Theme.Light)
// .WithCustomThemes(CustomThemeMode.None));
//});
+
+ // 19. Pinnable filter bar
+ app.UseSwaggerUI(Theme.Dark, c =>
+ {
+ c.DocumentTitle = "Sample API - Pinnable Filter Bar";
+ c.EnableAllAdvancedOptions();
+ c.EnableFilter(pinnable: true);
+ });
}
app.UseHttpsRedirection();
diff --git a/samples/Sample.AspNetCore.SwaggerUI.Swashbuckle/Sample.AspNetCore.SwaggerUI.Swashbuckle.csproj b/samples/Sample.AspNetCore.SwaggerUI.Swashbuckle/Sample.AspNetCore.SwaggerUI.Swashbuckle.csproj
index 3b05d88..c825bf9 100644
--- a/samples/Sample.AspNetCore.SwaggerUI.Swashbuckle/Sample.AspNetCore.SwaggerUI.Swashbuckle.csproj
+++ b/samples/Sample.AspNetCore.SwaggerUI.Swashbuckle/Sample.AspNetCore.SwaggerUI.Swashbuckle.csproj
@@ -9,8 +9,8 @@
-
-
+
+
diff --git a/samples/Sample.NSwag.AspNetCore.Extensions/Controllers/OrdersController.cs b/samples/Sample.NSwag.AspNetCore.Extensions/Controllers/OrdersController.cs
new file mode 100644
index 0000000..8db5b48
--- /dev/null
+++ b/samples/Sample.NSwag.AspNetCore.Extensions/Controllers/OrdersController.cs
@@ -0,0 +1,27 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+[Tags("Orders")]
+public class OrdersController : ControllerBase
+{
+ ///
+ /// Gets all orders
+ ///
+ [HttpGet]
+ public IActionResult GetAll()
+ {
+ return Ok(new[] { new { Id = 1, Total = 150.00 }, new { Id = 2, Total = 200.00 } });
+ }
+
+ ///
+ /// Gets an order by ID
+ ///
+ [HttpGet("{id}")]
+ public IActionResult GetById(int id)
+ {
+ return Ok(new { Id = id, Total = 150.00, Status = "Pending" });
+ }
+}
\ No newline at end of file
diff --git a/samples/Sample.NSwag.AspNetCore.Extensions/Controllers/ProductsController.cs b/samples/Sample.NSwag.AspNetCore.Extensions/Controllers/ProductsController.cs
new file mode 100644
index 0000000..ef7b567
--- /dev/null
+++ b/samples/Sample.NSwag.AspNetCore.Extensions/Controllers/ProductsController.cs
@@ -0,0 +1,36 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+[Tags("Products")]
+public class ProductsController : ControllerBase
+{
+ ///
+ /// Gets all products
+ ///
+ [HttpGet]
+ public IActionResult GetAll()
+ {
+ return Ok(new[] { new { Id = 1, Name = "Product A" }, new { Id = 2, Name = "Product B" } });
+ }
+
+ ///
+ /// Gets a product by ID
+ ///
+ [HttpGet("{id}")]
+ public IActionResult GetById(int id)
+ {
+ return Ok(new { Id = id, Name = "Product", Price = 99.99 });
+ }
+
+ ///
+ /// Creates a new product
+ ///
+ [HttpPost]
+ public IActionResult Create([FromBody] object product)
+ {
+ return CreatedAtAction(nameof(GetById), new { id = 1 }, product);
+ }
+}
\ No newline at end of file
diff --git a/samples/Sample.NSwag.AspNetCore.Extensions/Controllers/UsersController.cs b/samples/Sample.NSwag.AspNetCore.Extensions/Controllers/UsersController.cs
new file mode 100644
index 0000000..68160dd
--- /dev/null
+++ b/samples/Sample.NSwag.AspNetCore.Extensions/Controllers/UsersController.cs
@@ -0,0 +1,54 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+[Tags("Users")]
+public class UsersController : ControllerBase
+{
+ ///
+ /// Gets all users
+ ///
+ [HttpGet]
+ public IActionResult GetAll()
+ {
+ return Ok(new[] { new { Id = 1, Name = "John" }, new { Id = 2, Name = "Jane" } });
+ }
+
+ ///
+ /// Gets a user by ID
+ ///
+ [HttpGet("{id}")]
+ public IActionResult GetById(int id)
+ {
+ return Ok(new { Id = id, Name = "John Doe" });
+ }
+
+ ///
+ /// Creates a new user
+ ///
+ [HttpPost]
+ public IActionResult Create([FromBody] object user)
+ {
+ return CreatedAtAction(nameof(GetById), new { id = 1 }, user);
+ }
+
+ ///
+ /// Updates an existing user
+ ///
+ [HttpPut("{id}")]
+ public IActionResult Update(int id, [FromBody] object user)
+ {
+ return NoContent();
+ }
+
+ ///
+ /// Deletes a user
+ ///
+ [HttpDelete("{id}")]
+ public IActionResult Delete(int id)
+ {
+ return NoContent();
+ }
+}
\ No newline at end of file
diff --git a/samples/Sample.NSwag.AspNetCore.Extensions/Program.cs b/samples/Sample.NSwag.AspNetCore.Extensions/Program.cs
new file mode 100644
index 0000000..508b5f8
--- /dev/null
+++ b/samples/Sample.NSwag.AspNetCore.Extensions/Program.cs
@@ -0,0 +1,30 @@
+var builder = WebApplication.CreateBuilder(args);
+builder.Services.AddControllers();
+
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddOpenApiDocument(c =>
+{
+ c.Title = "Sample API - Extensions Demo";
+ c.Version = "v1";
+
+ // API Counter Extension
+ // Automatically appends operation count to tag descriptions
+ c.AppendOperationCountToTags();
+
+ // Or with custom template:
+ // c.AppendOperationCountToTags(" [{0} endpoints]");
+ // c.AppendOperationCountToTags(" (APIs: {0})");
+});
+
+var app = builder.Build();
+
+if (app.Environment.IsDevelopment())
+{
+ app.UseOpenApi();
+ app.UseSwaggerUi();
+}
+
+app.UseHttpsRedirection();
+app.MapControllers();
+
+await app.RunAsync();
\ No newline at end of file
diff --git a/samples/Sample.NSwag.AspNetCore.Extensions/Properties/launchSettings.json b/samples/Sample.NSwag.AspNetCore.Extensions/Properties/launchSettings.json
new file mode 100644
index 0000000..bc6351d
--- /dev/null
+++ b/samples/Sample.NSwag.AspNetCore.Extensions/Properties/launchSettings.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5101",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7101;http://localhost:5101",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/samples/Sample.NSwag.AspNetCore.Extensions/Sample.NSwag.AspNetCore.Extensions.csproj b/samples/Sample.NSwag.AspNetCore.Extensions/Sample.NSwag.AspNetCore.Extensions.csproj
new file mode 100644
index 0000000..4980a36
--- /dev/null
+++ b/samples/Sample.NSwag.AspNetCore.Extensions/Sample.NSwag.AspNetCore.Extensions.csproj
@@ -0,0 +1,27 @@
+ο»Ώ
+
+
+
+
+ true
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/Sample.NSwag.AspNetCore.Extensions/appsettings.Development.json b/samples/Sample.NSwag.AspNetCore.Extensions/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/samples/Sample.NSwag.AspNetCore.Extensions/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/samples/Sample.NSwag.AspNetCore.Extensions/appsettings.json b/samples/Sample.NSwag.AspNetCore.Extensions/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/samples/Sample.NSwag.AspNetCore.Extensions/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/AdvancedOptions.cs b/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/AdvancedOptions.cs
index e7bf728..dbd37ed 100644
--- a/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/AdvancedOptions.cs
+++ b/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/AdvancedOptions.cs
@@ -6,6 +6,7 @@
internal static class AdvancedOptions
{
internal const string PinnableTopbar = nameof(PinnableTopbar);
+ internal const string PinnableFilterBar = nameof(PinnableFilterBar);
internal const string BackToTop = nameof(BackToTop);
internal const string StickyOperations = nameof(StickyOperations);
internal const string ExpandOrCollapseAllOperations = nameof(ExpandOrCollapseAllOperations);
@@ -18,12 +19,14 @@ internal static class AdvancedOptions
// JS placeholders
internal const string PinnableTopbarJsPH = "{$PINNABLE_TOPBAR}";
+ internal const string PinnableFilterBarJsPH = "{$PINNABLE_FILTER_BAR}";
internal const string BackToTopJsPH = "{$BACK_TO_TOP}";
internal const string ExpandOrCollapseAllOperationsJsPH = "{$EXPAND_COLLAPSE_ALL_OPERATIONS}";
internal const string ThemeSwitcherJsPH = "{$THEME_SWITCHER}";
private static readonly string[][] s_jsDependentFeatures = [
[PinnableTopbar, PinnableTopbarJsPH],
+ [PinnableFilterBar, PinnableFilterBarJsPH],
[BackToTop, BackToTopJsPH],
[ExpandOrCollapseAllOperations, ExpandOrCollapseAllOperationsJsPH],
[ThemeSwitcher, ThemeSwitcherJsPH]
@@ -34,6 +37,11 @@ internal static void EnablePinnableTopbar(this IDictionary optio
options.TryAdd(PinnableTopbar, true);
}
+ internal static void EnablePinnableFilterBar(this IDictionary options)
+ {
+ options.TryAdd(PinnableFilterBar, true);
+ }
+
internal static void EnableBackToTop(this IDictionary options)
{
options.TryAdd(BackToTop, true);
diff --git a/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/FileProvider.cs b/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/FileProvider.cs
index 9f5915c..db6d12b 100644
--- a/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/FileProvider.cs
+++ b/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/FileProvider.cs
@@ -1,7 +1,9 @@
ο»Ώusing Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
+using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Reflection;
+using System.Runtime.CompilerServices;
using System.Text.Json;
namespace AspNetCore.Swagger.Themes;
@@ -21,8 +23,12 @@ internal static class FileProvider
internal const string ThemeMetadataPath = "/themes/metadata.json";
internal const string JsFilename = "ui.min.js";
- // Track registered endpoints to prevent duplicates
- private static readonly HashSet s_registeredEndpoints = new(StringComparer.OrdinalIgnoreCase);
+ // Track registered endpoint paths to prevent duplicate registration across the process
+ private static readonly ConcurrentDictionary s_registeredEndpoints = new(StringComparer.OrdinalIgnoreCase);
+
+ // Per-app dispatch table backing the single middleware registered for classic IApplicationBuilder hosts.
+ // WebApplication hosts are served through routing (MapGet) instead, so they never need an entry here.
+ private static readonly ConditionalWeakTable> s_appDispatchTables = new();
private static FrozenSet s_frozenEndpoints;
@@ -115,11 +121,14 @@ internal static string GetResourceText(string fileName, Assembly assembly, out s
///
internal static void AddGetEndpoint(IApplicationBuilder app, string path, string content, string contentType = MimeTypes.Text.Css)
{
- if (!s_registeredEndpoints.Add(path))
+ if (!s_registeredEndpoints.TryAdd(path, 0))
return;
if (app is WebApplication webApp)
{
+ // WebApplication hosts are served through ASP.NET Core's routing tree, which already
+ // gives O(1)-ish lookup regardless of how many themes are registered, and MapGet lets
+ // us bypass any global fallback authorization policy via AllowAnonymous.
webApp.MapGet(path, (HttpContext context) =>
{
SetCacheHeaders(context);
@@ -127,23 +136,35 @@ internal static void AddGetEndpoint(IApplicationBuilder app, string path, string
})
.ExcludeFromDescription()
.AllowAnonymous();
+
+ return;
}
- else
+
+ // Register the single dispatch middleware only the first time this app instance is seen.
+ // Every subsequent call just adds an entry to this app's own dispatch table, so requests
+ // are only ever served for paths that this specific app instance registered.
+ var dispatchTable = s_appDispatchTables.GetValue(app, static application =>
{
- app.Use(async (context, next) =>
+ var table = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
+
+ application.Use(async (context, next) =>
{
- if (context.Request.Path.Equals(path, StringComparison.OrdinalIgnoreCase))
+ if (table.TryGetValue(context.Request.Path.Value ?? string.Empty, out var endpoint))
{
SetCacheHeaders(context);
- context.Response.ContentType = contentType;
- await context.Response.WriteAsync(content);
+ context.Response.ContentType = endpoint.ContentType;
+ await context.Response.WriteAsync(endpoint.Content);
}
else
{
await next();
}
});
- }
+
+ return table;
+ });
+
+ dispatchTable[path] = (content, contentType);
static void SetCacheHeaders(HttpContext context)
{
@@ -161,7 +182,7 @@ internal static void ExposeThemeMetadata(
string currentThemeName,
string displayFormat)
{
- if (s_registeredEndpoints.Contains(ThemeMetadataPath))
+ if (s_registeredEndpoints.ContainsKey(ThemeMetadataPath))
return;
var themeList = themes.Select(rt => new
@@ -199,7 +220,7 @@ internal static void ExposeThemeMetadata(
/// Freezes collections after startup for better read performance.
///
internal static void FreezeCollections() =>
- s_frozenEndpoints ??= s_registeredEndpoints.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
+ s_frozenEndpoints ??= s_registeredEndpoints.Keys.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
///
/// Checks if a theme name is predefined.
diff --git a/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Scripts/ui.js b/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Scripts/ui.js
index 1055e75..fbfbde1 100644
--- a/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Scripts/ui.js
+++ b/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Scripts/ui.js
@@ -13,8 +13,12 @@ window.onpageshow = function () {
console.log('Hello Swagger UI!');
+ suppressNativeDarkModeClass();
+
setUpPinnableTopbar({$PINNABLE_TOPBAR});
+ setUpPinnableFilterBar({$PINNABLE_FILTER_BAR});
+
setUpScrollToTopButton({$BACK_TO_TOP});
setUpExpandAndCollapseOperationsButtons({$EXPAND_COLLAPSE_ALL_OPERATIONS});
@@ -28,30 +32,149 @@ function setUpPinnableTopbar(enabled) {
if (enabled === false)
return;
- const topbarWrapper = document.querySelector('.topbar-wrapper');
+ setUpPinnableToggle({
+ buttonId: 'pin-topbar-btn',
+ container: document.querySelector('.topbar-wrapper'),
+ // 3 parentNode hops up from the button (appended to .topbar-wrapper) reaches .topbar.
+ pinnedAncestorDepth: 3,
+ storageKey: 'swaggerui-pinnable-topbar-preference',
+ pinTitle: 'Pin topbar',
+ unpinTitle: 'Unpin topbar',
+ // No saved preference defaults to pinned, matching the topbar's original
+ // always-pinned-on-load behavior.
+ defaultPinned: true
+ });
+}
+
+// Swashbuckle.AspNetCore 10.x's swagger-ui-dist ships its own dark-mode toggle, hidden via CSS
+// (see common.css) because it layers swagger-ui's own dark styling on top of this library's
+// theme. Hiding the button doesn't stop the toggle's React component from mounting though: it
+// still auto-applies a 'dark-mode' class to based on prefers-color-scheme regardless of
+// the button's visibility. A MutationObserver strips it back off whenever it appears, since
+// there's no reliable synchronous point to catch it once and be done - the toggle component can
+// mount before or after this script runs.
+function suppressNativeDarkModeClass() {
+ // Only remove (and thus only mutate the attribute) when the class is actually present, so
+ // this can never trigger the observer with a mutation of its own.
+ const strip = () => {
+ if (rootElement.classList.contains('dark-mode'))
+ rootElement.classList.remove('dark-mode');
+ };
+ strip();
+ new MutationObserver(strip).observe(rootElement, { attributes: true, attributeFilter: ['class'] });
+}
+
+function setUpPinnableFilterBar(enabled) {
+ if (enabled === false)
+ return;
+
+ // Unlike #swagger-ui itself, .filter only renders once the OpenAPI document has been
+ // fetched and parsed - it commonly doesn't exist yet at the moment #swagger-ui does, so a
+ // single lookup here would silently and permanently no-op. Poll for it with the same
+ // bounded-retry pattern used by setUpExpandAndCollapseOperationsButtons below.
+ const MAX_ATTEMPTS = 60;
+ const RETRY_DELAY_MS = 200;
+ let attempts = 0;
- const pinTopbarBtn = document.createElement('button');
- pinTopbarBtn.setAttribute('id', 'pin-topbar-btn');
- pinTopbarBtn.addEventListener('click', () => pinOrUnpinTopbar(pinTopbarBtn))
+ (function waitForFilterBar() {
+ attempts++;
+ const filterBar = document.querySelector('.filter');
- topbarWrapper.appendChild(pinTopbarBtn);
+ if (filterBar) {
+ initPinnableFilterBar(filterBar);
+ return;
+ }
- pinOrUnpinTopbar(pinTopbarBtn);
+ if (attempts < MAX_ATTEMPTS)
+ setTimeout(waitForFilterBar, RETRY_DELAY_MS);
+ })();
}
-function pinOrUnpinTopbar(pinTopbarBtn) {
- if (pinTopbarBtn.parentNode.parentNode.parentNode.classList.contains('pinned')) {
- pinTopbarBtn.parentNode.parentNode.parentNode.classList.remove('pinned');
- setUnpinnedIconTo(pinTopbarBtn);
- pinTopbarBtn.setAttribute('title', 'Pin topbar');
+function initPinnableFilterBar(filterBar) {
+ // CSS alone can't tell whether a sticky element has actually reached its sticky top, so
+ // toggle a 'stuck' class here (styled in common.css) by comparing the sticky wrapper's
+ // viewport position against its computed sticky offset. The wrapper is 2 parentElement
+ // hops up from .filter (.filter -> .filter-container -> sticky div, see common.css).
+ const stickyWrapper = filterBar.parentElement?.parentElement;
+
+ function updateStuckState() {
+ if (!stickyWrapper)
+ return;
+
+ // getComputedStyle().top is 'auto' (NaN) while unpinned, so the comparison alone
+ // could never be true then; the explicit pinned check just makes that intent clear.
+ const stuck = filterBar.classList.contains('pinned')
+ && stickyWrapper.getBoundingClientRect().top <= parseFloat(getComputedStyle(stickyWrapper).top) + 1;
+ filterBar.classList.toggle('stuck', stuck);
}
- else {
- pinTopbarBtn.parentNode.parentNode.parentNode.classList.add('pinned');
- setPinnedIconTo(pinTopbarBtn);
- pinTopbarBtn.setAttribute('title', 'Unpin topbar');
+
+ window.addEventListener('scroll', updateStuckState, { passive: true });
+ window.addEventListener('resize', updateStuckState);
+
+ setUpPinnableToggle({
+ buttonId: 'pin-filterbar-btn',
+ container: filterBar,
+ // 1 parentNode hop up from the button (appended directly to .filter) reaches .filter itself.
+ pinnedAncestorDepth: 1,
+ storageKey: 'swaggerui-pinnable-filterbar-preference',
+ pinTitle: 'Pin filter bar',
+ unpinTitle: 'Unpin filter bar',
+ defaultPinned: false,
+ // Re-evaluate on every pin/unpin so unpinning mid-scroll clears the 'stuck' class
+ // immediately instead of waiting for the next scroll event.
+ onPinnedStateApplied: updateStuckState
+ });
+}
+
+// Shared behavior behind every pinnable element (topbar, filter bar, ...): injects a pin
+// button into `container`, toggles a 'pinned' class on the ancestor `pinnedAncestorDepth`
+// parentNodes up from that button, persists the choice to localStorage, and notifies the
+// optional `onPinnedStateApplied` callback after every applied state (initial load included).
+function setUpPinnableToggle({ buttonId, container, pinnedAncestorDepth, storageKey, pinTitle, unpinTitle, defaultPinned, onPinnedStateApplied }) {
+ if (!container)
+ return;
+
+ const pinBtn = document.createElement('button');
+ pinBtn.setAttribute('id', buttonId);
+ pinBtn.addEventListener('click', () => {
+ const currentlyPinned = pinnedAncestorOf(pinBtn).classList.contains('pinned');
+ applyPinnedState(!currentlyPinned, true);
+ pinBtn?.blur();
+ })
+
+ container.appendChild(pinBtn);
+
+ // Apply the saved (or default) pinned state directly, so there's no flash of the wrong
+ // state on page load.
+ const savedState = localStorage.getItem(storageKey);
+ const isPinned = savedState !== null ? savedState === 'pinned' : defaultPinned;
+ applyPinnedState(isPinned, false);
+
+ function pinnedAncestorOf(button) {
+ let ancestor = button;
+ for (let i = 0; i < pinnedAncestorDepth; i++)
+ ancestor = ancestor.parentNode;
+ return ancestor;
}
- pinTopbarBtn?.blur();
+ function applyPinnedState(pinned, saveToStorage) {
+ if (pinned) {
+ pinnedAncestorOf(pinBtn).classList.add('pinned');
+ setPinnedIconTo(pinBtn);
+ pinBtn.setAttribute('title', unpinTitle);
+ }
+ else {
+ pinnedAncestorOf(pinBtn).classList.remove('pinned');
+ setUnpinnedIconTo(pinBtn);
+ pinBtn.setAttribute('title', pinTitle);
+ }
+
+ if (saveToStorage) {
+ localStorage.setItem(storageKey, pinned ? 'pinned' : 'unpinned');
+ }
+
+ onPinnedStateApplied?.();
+ }
}
function setPinnedIconTo(element) {
diff --git a/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Scripts/ui.min.js b/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Scripts/ui.min.js
index fd477c8..aa56ee4 100644
--- a/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Scripts/ui.min.js
+++ b/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Scripts/ui.min.js
@@ -1,5 +1 @@
-ο»Ώ/*Swagger UI https://github.com/teociaps/SwaggerUI.Themes */const rootElement=document.documentElement;function setUpPinnableTopbar(e){if(!1===e)return;let t=document.querySelector(".topbar-wrapper"),n=document.createElement("button");n.setAttribute("id","pin-topbar-btn"),n.addEventListener("click",()=>pinOrUnpinTopbar(n)),t.appendChild(n),pinOrUnpinTopbar(n)}function pinOrUnpinTopbar(e){e.parentNode.parentNode.parentNode.classList.contains("pinned")?(e.parentNode.parentNode.parentNode.classList.remove("pinned"),setUnpinnedIconTo(e),e.setAttribute("title","Pin topbar")):(e.parentNode.parentNode.parentNode.classList.add("pinned"),setPinnedIconTo(e),e.setAttribute("title","Unpin topbar")),e?.blur()}function setPinnedIconTo(e){e.innerHTML=``}function setUnpinnedIconTo(e){e.innerHTML=``}function setUpScrollToTopButton(e){if(!1===e)return;let t=document.createElement("div");t.classList.add("scroll-to-top-wrapper");let n=document.createElement("button");n.setAttribute("id","scroll-to-top-btn"),n.setAttribute("title","Back to top"),n.addEventListener("click",()=>{scrollToTop(),n?.blur()}),t.appendChild(n);let l=document.getElementById("swagger-ui");l.appendChild(t);let r=()=>{window.scrollY>=200?n.classList.add("showBtn"):n.classList.remove("showBtn")};window.addEventListener("scroll",r),window.addEventListener("resize",r)}function scrollToTop(){rootElement.scrollTo({top:0,behavior:"smooth"})}function setUpExpandAndCollapseOperationsButtons(e){if(!1===e)return;let t=0,n=document.getElementById("swagger-ui")||document.querySelector(".swagger-ui");function l(){let e=document.querySelectorAll(".opblock-tag-section");return!!e&&0!==e.length&&(e.forEach(e=>(function e(t){if(!t||"true"===t.dataset.expandCollapseAttached)return;let n=t.querySelector("h3"),l=n?.querySelector("button.expand-operation");if(n&&!n.querySelector(".expand-collapse-all-btn")){let r=document.createElement("button");r.setAttribute("title","Expand/Collapse all the operations"),r.classList.add("expand-collapse-all-btn"),r.innerHTML="Expand/Collapse All",l?l.before(r):n.appendChild(r),r.addEventListener("click",e=>{e.preventDefault(),e.stopPropagation();let n=t.querySelectorAll(".opblock .opblock-control-arrow");if(!n||0===n.length)return;let l=Array.from(n).every(e=>"true"===e.getAttribute("aria-expanded"));l?n.forEach(e=>{"true"===e.getAttribute("aria-expanded")&&e.click()}):n.forEach(e=>{"false"===e.getAttribute("aria-expanded")&&e.click()})})}t.dataset.expandCollapseAttached="true"})(e)),!0)}!function e(){t++;let r=l();if(r){try{let a=n||document.body,o=new MutationObserver(e=>{for(let t of e)t.addedNodes&&t.addedNodes.length>0&&l()});o.observe(a,{childList:!0,subtree:!0})}catch(i){}return}t<60&&setTimeout(e,200)}()}function setUpThemeSwitcher(e){if(!1===e)return;let t="swaggerui-theme-preference",n=null,l=null;function r(e,r){let a=n?.themes.find(t=>t.name===e);if(!a){console.warn(`[ThemeSwitcher] Theme not found: ${e}`);return}let o=document.querySelectorAll('link[rel="stylesheet"]'),i=n.themes.map(e=>e.cssPath),s=!1;o.forEach(t=>{let n=t.dataset.theme||i.some(e=>t.href?.endsWith(e));if(n){let l=t.dataset.theme===e||t.href?.endsWith(a.cssPath);l?(t.disabled=!1,t.dataset.theme=e,s=!0):t.disabled=!0}}),s||console.warn(`[ThemeSwitcher] Could not activate theme: ${e}`),l=e,r&&localStorage.setItem(t,e);let $=document.getElementById("theme-switcher-select");$&&$.value!==e&&($.value=e)}fetch("/themes/metadata.json").then(e=>e.ok?e.json():(console.warn("[ThemeSwitcher] Failed to load theme metadata"),null)).then(e=>{if(!e?.themes||e.themes.length<2){console.warn("[ThemeSwitcher] Not enough themes available for switcher");return}n=e,l=detectCurrentTheme(e.themes),function e(n){let a=localStorage.getItem(t);a&&n.some(e=>e.name===a)&&a!==l&&r(a,!1)}(e.themes),function e(t){let n=document.querySelector(".topbar-wrapper");if(!n){console.warn("[ThemeSwitcher] Topbar not found");return}let a=document.createElement("select");a.id="theme-switcher-select",a.setAttribute("aria-label","Select theme"),a.title="Switch theme",t.themes.forEach(e=>{let n=document.createElement("option");n.value=e.name,n.textContent=formatThemeName(e.name,t.config?.displayFormat||"{name}"),e.name===l&&(n.selected=!0),a.appendChild(n)}),a.addEventListener("change",e=>{r(e.target.value,!0)});let o=document.getElementById("pin-topbar-btn");o?o.before(a):n.appendChild(a)}(e)}).catch(e=>{console.error("[ThemeSwitcher] Error loading themes:",e)})}function detectCurrentTheme(e){let t=document.querySelector('link[rel="stylesheet"]:not([disabled])[data-theme]');if(t?.dataset.theme)return t.dataset.theme;let n=document.querySelectorAll('link[rel="stylesheet"]:not([disabled])');for(let l of n){let r=e.find(e=>!!l.href&&(l.href.includes(e.cssPath)||l.href.endsWith(e.cssPath)||l.href.includes(e.name.toLowerCase())));if(r)return r.name}return e.length>0?e[0].name:null}function formatThemeName(e,t){let n=e.replaceAll(/([A-Z])/g," $1").trim().split(/[\s_-]+/).map(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()).join(" ");return t.replace("{name}",n)}window.onpageshow=function(){let e=setInterval(function(){null!=document.getElementById("swagger-ui")&&(clearInterval(e),console.log("Hello Swagger UI!"),setUpPinnableTopbar({$PINNABLE_TOPBAR}),setUpScrollToTopButton({$BACK_TO_TOP}),setUpExpandAndCollapseOperationsButtons({$EXPAND_COLLAPSE_ALL_OPERATIONS}),setUpThemeSwitcher({$THEME_SWITCHER}))},100)};
\ No newline at end of file
+ο»Ώ/*Swagger UI https://github.com/teociaps/SwaggerUI.Themes */const rootElement=document.documentElement;function setUpPinnableTopbar(e){!1!==e&&setUpPinnableToggle({buttonId:"pin-topbar-btn",container:document.querySelector(".topbar-wrapper"),pinnedAncestorDepth:3,storageKey:"swaggerui-pinnable-topbar-preference",pinTitle:"Pin topbar",unpinTitle:"Unpin topbar",defaultPinned:!0})}function suppressNativeDarkModeClass(){const e=()=>{rootElement.classList.contains("dark-mode")&&rootElement.classList.remove("dark-mode")};e(),new MutationObserver(e).observe(rootElement,{attributes:!0,attributeFilter:["class"]})}function setUpPinnableFilterBar(e){if(!1===e)return;let n=0;!function r(){n++;const t=document.querySelector(".filter");if(t)return void initPinnableFilterBar(t);n<60&&setTimeout(r,200)}()}function initPinnableFilterBar(t){const n=t.parentElement?.parentElement;function o(){if(!n)return;const e=t.classList.contains("pinned")&&n.getBoundingClientRect().top<=parseFloat(getComputedStyle(n).top)+1;t.classList.toggle("stuck",e)}window.addEventListener("scroll",o,{passive:!0}),window.addEventListener("resize",o),setUpPinnableToggle({buttonId:"pin-filterbar-btn",container:t,pinnedAncestorDepth:1,storageKey:"swaggerui-pinnable-filterbar-preference",pinTitle:"Pin filter bar",unpinTitle:"Unpin filter bar",defaultPinned:!1,onPinnedStateApplied:o})}function setUpPinnableToggle({buttonId:e,container:t,pinnedAncestorDepth:n,storageKey:o,pinTitle:l,unpinTitle:a,defaultPinned:r,onPinnedStateApplied:p}){if(!t)return;const i=document.createElement("button");i.setAttribute("id",e),i.addEventListener("click",()=>{d(!c(i).classList.contains("pinned"),!0),i?.blur()}),t.appendChild(i);const s=localStorage.getItem(o);function c(e){let t=e;for(let e=0;e\n \n '}function setUnpinnedIconTo(e){e.innerHTML=''}function setUpScrollToTopButton(e){if(!1===e)return;const t=document.createElement("div");t.classList.add("scroll-to-top-wrapper");const n=document.createElement("button");n.setAttribute("id","scroll-to-top-btn"),n.setAttribute("title","Back to top"),n.addEventListener("click",()=>{scrollToTop(),n?.blur()}),t.appendChild(n);document.getElementById("swagger-ui").appendChild(t);const o=()=>{window.scrollY>=200?n.classList.add("showBtn"):n.classList.remove("showBtn")};window.addEventListener("scroll",o),window.addEventListener("resize",o)}function scrollToTop(){rootElement.scrollTo({top:0,behavior:"smooth"})}function setUpExpandAndCollapseOperationsButtons(e){if(!1===e)return;let t=0;const n=document.getElementById("swagger-ui")||document.querySelector(".swagger-ui");function o(){const e=document.querySelectorAll(".opblock-tag-section");return!(!e||0===e.length)&&(e.forEach(e=>function(e){if(!e||"true"===e.dataset.expandCollapseAttached)return;const t=e.querySelector("h3"),n=t?.querySelector("button.expand-operation");if(t&&!t.querySelector(".expand-collapse-all-btn")){const o=document.createElement("button");o.setAttribute("title","Expand/Collapse all the operations"),o.classList.add("expand-collapse-all-btn"),o.innerHTML="Expand/Collapse All",n?n.before(o):t.appendChild(o),o.addEventListener("click",t=>{t.preventDefault(),t.stopPropagation();const n=e.querySelectorAll(".opblock .opblock-control-arrow");n&&0!==n.length&&(Array.from(n).every(e=>"true"===e.getAttribute("aria-expanded"))?n.forEach(e=>{"true"===e.getAttribute("aria-expanded")&&e.click()}):n.forEach(e=>{"false"===e.getAttribute("aria-expanded")&&e.click()}))})}e.dataset.expandCollapseAttached="true"}(e)),!0)}!function e(){if(t++,o())try{const e=n||document.body;new MutationObserver(e=>{for(const t of e)t.addedNodes&&t.addedNodes.length>0&&o()}).observe(e,{childList:!0,subtree:!0})}catch(e){}else t<60&&setTimeout(e,200)}()}function setUpThemeSwitcher(e){if(!1===e)return;const t="swaggerui-theme-preference";let n=null,o=null;function l(e,l){const a=n?.themes.find(t=>t.name===e);if(!a)return void console.warn(`[ThemeSwitcher] Theme not found: ${e}`);const r=document.querySelectorAll('link[rel="stylesheet"]'),i=n.themes.map(e=>e.cssPath);let s=!1;r.forEach(t=>{if(t.dataset.theme||i.some(e=>t.href?.endsWith(e))){t.dataset.theme===e||t.href?.endsWith(a.cssPath)?(t.disabled=!1,t.dataset.theme=e,s=!0):t.disabled=!0}}),s||console.warn(`[ThemeSwitcher] Could not activate theme: ${e}`),o=e,l&&localStorage.setItem(t,e);const c=document.getElementById("theme-switcher-select");c&&c.value!==e&&(c.value=e)}fetch("/themes/metadata.json").then(e=>e.ok?e.json():(console.warn("[ThemeSwitcher] Failed to load theme metadata"),null)).then(e=>{!e?.themes||e.themes.length<2?console.warn("[ThemeSwitcher] Not enough themes available for switcher"):(n=e,o=detectCurrentTheme(e.themes),function(e){const n=localStorage.getItem(t);n&&e.some(e=>e.name===n)&&n!==o&&l(n,!1)}(e.themes),function(e){const t=document.querySelector(".topbar-wrapper");if(!t)return void console.warn("[ThemeSwitcher] Topbar not found");const n=document.createElement("select");n.id="theme-switcher-select",n.setAttribute("aria-label","Select theme"),n.title="Switch theme",e.themes.forEach(t=>{const l=document.createElement("option");l.value=t.name,l.textContent=formatThemeName(t.name,e.config?.displayFormat||"{name}"),t.name===o&&(l.selected=!0),n.appendChild(l)}),n.addEventListener("change",e=>{l(e.target.value,!0)});const a=document.getElementById("pin-topbar-btn");a?a.before(n):t.appendChild(n)}(e))}).catch(e=>{console.error("[ThemeSwitcher] Error loading themes:",e)})}function detectCurrentTheme(e){const t=document.querySelector('link[rel="stylesheet"]:not([disabled])[data-theme]');if(t?.dataset.theme)return t.dataset.theme;const n=document.querySelectorAll('link[rel="stylesheet"]:not([disabled])');for(const t of n){const n=e.find(e=>!!t.href&&(t.href.includes(e.cssPath)||t.href.endsWith(e.cssPath)||t.href.includes(e.name.toLowerCase())));if(n)return n.name}return e.length>0?e[0].name:null}function formatThemeName(e,t){const n=e.replaceAll(/([A-Z])/g," $1").trim().split(/[\s_-]+/).map(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()).join(" ");return t.replace("{name}",n)}window.onpageshow=function(){let e=setInterval(function(){null!=document.getElementById("swagger-ui")&&(clearInterval(e),console.log("Hello Swagger UI!"),suppressNativeDarkModeClass(),setUpPinnableTopbar({$PINNABLE_TOPBAR}),setUpPinnableFilterBar({$PINNABLE_FILTER_BAR}),setUpScrollToTopButton({$BACK_TO_TOP}),setUpExpandAndCollapseOperationsButtons({$EXPAND_COLLAPSE_ALL_OPERATIONS}),setUpThemeSwitcher({$THEME_SWITCHER}))},100)};
\ No newline at end of file
diff --git a/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Styles/common.css b/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Styles/common.css
index 7a42fe6..30e94c0 100644
--- a/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Styles/common.css
+++ b/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Styles/common.css
@@ -6,6 +6,15 @@
@charset "UTF-8";
+:root {
+ /* Rendered heights of the pinned topbar/filter bar, used to stack sticky elements below
+ them without overlap. Declared once here so the two DOM-branch-specific copies below
+ (needed because of how .topbar.pinned/.filter.pinned relate to the elements that read
+ them) can't drift out of sync. */
+ --topbar-pinned-height: 4rem;
+ --filterbar-pinned-height: 5rem;
+}
+
*::-webkit-scrollbar {
width: 16px;
}
@@ -95,6 +104,15 @@ body {
.swagger-ui .topbar.pinned {
position: sticky;
top: 10px;
+ --pinned-topbar-offset: var(--topbar-pinned-height);
+ }
+
+ /* The filter bar and the sticky operation tag headers live in a different branch of the
+ DOM than the topbar, so they can't inherit --pinned-topbar-offset directly from
+ .topbar.pinned. Re-declare it here, on their closest common ancestor, so it only
+ carries a nonzero value while the topbar is actually pinned. */
+ .swagger-ui:has(> .topbar.pinned) {
+ --pinned-topbar-offset: var(--topbar-pinned-height);
}
.swagger-ui .topbar.pinned::before {
@@ -122,7 +140,10 @@ body {
fill: var(--topbar-pin-icon-color, white);
}
- .swagger-ui .topbar .topbar-wrapper #pin-topbar-btn {
+ /* Base/hover/active button styling is shared with the filter bar's pin button
+ (#pin-filterbar-btn below); only the icon fill color differs between the two. */
+ .swagger-ui .topbar .topbar-wrapper #pin-topbar-btn,
+ .swagger-ui .filter #pin-filterbar-btn {
cursor: pointer;
background: transparent;
border: none;
@@ -133,12 +154,15 @@ body {
}
.swagger-ui .topbar .topbar-wrapper #pin-topbar-btn:hover,
- .swagger-ui .topbar .topbar-wrapper #pin-topbar-btn:focus-within {
+ .swagger-ui .topbar .topbar-wrapper #pin-topbar-btn:focus-within,
+ .swagger-ui .filter #pin-filterbar-btn:hover,
+ .swagger-ui .filter #pin-filterbar-btn:focus-within {
scale: 1.05;
animation: pintobbar 1.2s infinite;
}
- .swagger-ui .topbar .topbar-wrapper #pin-topbar-btn:active {
+ .swagger-ui .topbar .topbar-wrapper #pin-topbar-btn:active,
+ .swagger-ui .filter #pin-filterbar-btn:active {
scale: 0.95;
}
@@ -149,13 +173,21 @@ body {
backdrop-filter: blur(20px);
}
- .swagger-ui .topbar.pinned .topbar-wrapper #pin-topbar-btn {
- translate: 0 5px;
+ /* rotate is shared with the filter bar's pinned pin button (#pin-filterbar-btn
+ below); translate is topbar-specific, nudging the button into the pinned layout. */
+ .swagger-ui .topbar.pinned .topbar-wrapper #pin-topbar-btn,
+ .swagger-ui .filter.pinned #pin-filterbar-btn {
rotate: z 0deg;
}
+ .swagger-ui .topbar.pinned .topbar-wrapper #pin-topbar-btn {
+ translate: 0 5px;
+ }
+
.swagger-ui .topbar.pinned .topbar-wrapper #pin-topbar-btn:hover,
- .swagger-ui .topbar.pinned .topbar-wrapper #pin-topbar-btn:focus-within {
+ .swagger-ui .topbar.pinned .topbar-wrapper #pin-topbar-btn:focus-within,
+ .swagger-ui .filter.pinned #pin-filterbar-btn:hover,
+ .swagger-ui .filter.pinned #pin-filterbar-btn:focus-within {
animation: unpintobbar 1.2s infinite;
}
@@ -251,6 +283,15 @@ body {
color: var(--topbar-download-url-button-color, #fff);
}
+/* Swashbuckle.AspNetCore 10.x's swagger-ui-dist ships its own native dark-mode toggle in the
+ topbar. It layers swagger-ui's own bundled dark CSS on top of whatever theme is active here,
+ visibly clashing with it - hide it since this library's own theme (and, when enabled, its
+ EnableThemeSwitcher) already cover light/dark. Harmless no-op on older Swashbuckle versions
+ that don't render this element at all. */
+.swagger-ui .topbar .dark-mode-toggle {
+ display: none;
+}
+
#theme-switcher-select {
background: var(--select-background-icon, #f7f7f7 url('data:image/svg+xml;charset=utf-8,') right 10px center no-repeat);
border: 2px solid var(--topbar-select-border-color, #62a03f);
@@ -402,8 +443,8 @@ body {
background: var(--opblock-tag-pinned-background-color-hover, #f1f1f1);
}
-.swagger-ui:has(> .topbar.pinned) .opblock-tag-section.is-open .opblock-tag {
- top: 4.2rem;
+.swagger-ui .opblock-tag-section.is-open .opblock-tag {
+ top: calc(var(--pinned-topbar-offset, 0px) + var(--pinned-filter-offset, 0px));
}
.swagger-ui .opblock {
@@ -646,10 +687,72 @@ body {
}
/* Other of operations */
+/* The pin button is appended as a sibling of .filter-container (swagger-ui-dist's own,
+ unstyled, full-width wrapper around the filter input) directly inside .filter. It's
+ absolutely positioned over the input's right end (like an input-embedded icon), with
+ position: relative here making .filter its containing block; align-items keeps its
+ static position vertically centered. */
+.swagger-ui .filter {
+ position: relative;
+ display: flex;
+ align-items: center;
+}
+
+ .swagger-ui .filter .filter-container {
+ flex: 1;
+ min-width: 0;
+ }
+
+ .swagger-ui .filter #pin-filterbar-btn {
+ position: absolute;
+ right: 1.5rem;
+ }
+
.swagger-ui .filter .operation-filter-input {
border: 2px solid var(--filter-operation-filter-input-border-color, #d8dde7);
}
+ /* Only while the pinned filter bar has actually reached its sticky top (the 'stuck'
+ class is toggled by ui.js β CSS can't detect that state): fake a bottom edge for the
+ sticky bar by stacking two hard shadows below the input β a background-colored fill,
+ then a 1px line in the operations' border color. */
+ .swagger-ui .filter.pinned.stuck .operation-filter-input {
+ box-shadow: 0 19px 0 0 var(--body-background-color, #fafafa), 0 20px 0 0 var(--opblock-tag-section-border-color, #dddddd);
+ }
+
+ /* .filter's own wrapper (.filter-container) is exactly as tall as the filter bar itself
+ (its only child), so making .filter, or .filter-container, position:sticky would have no
+ visible effect: a sticky box is bounded by its own containing block, and that block would
+ offer no extra room for it to remain "stuck" while the page scrolls. What actually needs
+ position: sticky is .filter-container's own parent, matched here by this selector (the
+ ">" combinators pin it down to exactly that element): unlike .filter-container, that div's
+ *own* parent, one level further up, spans the full page content (info box, filter,
+ operations list, models, ...) as siblings, which gives this element room to stick. It has
+ no stable class of its own in Swagger UI's markup, so it's targeted structurally via its
+ known descendants instead. */
+ .swagger-ui div:has(> .filter-container > .filter.pinned) {
+ position: sticky;
+ top: var(--pinned-topbar-offset, 0px);
+ z-index: 5;
+ background: var(--body-background-color, #fafafa);
+ --pinned-filter-offset: var(--filterbar-pinned-height);
+ }
+
+/* The sticky operation tag headers live in a different branch of the DOM than the filter bar,
+ so they can't inherit --pinned-filter-offset directly from the rule above. Re-declare it here,
+ on their closest common ancestor, so it only carries a nonzero value while the filter bar is
+ actually pinned. */
+.swagger-ui:has(.filter.pinned) {
+ --pinned-filter-offset: var(--filterbar-pinned-height);
+}
+
+ .swagger-ui .filter #pin-filterbar-btn svg {
+ fill: var(--filterbar-pin-icon-color, #707070);
+ }
+
+ /* Base, hover/focus, active, and pinned button styling is shared with the topbar's pin
+ button (#pin-topbar-btn) β see the .topbar .topbar-wrapper #pin-topbar-btn rules above. */
+
.swagger-ui .download-url-wrapper .failed,
.swagger-ui .filter .failed {
color: var(--filter-download-failed-color, red)
diff --git a/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Styles/common.min.css b/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Styles/common.min.css
index bd03f59..0e51dc6 100644
--- a/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Styles/common.min.css
+++ b/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Styles/common.min.css
@@ -1 +1 @@
-ο»Ώ/*Common Theme https://github.com/teociaps/SwaggerUI.Themes */@charset "UTF-8";*,:focus{font-family:system-ui,sans-serif}.swagger-ui,.swagger-ui .checkbox p,.swagger-ui .dialog-ux .modal-ux-content h4,.swagger-ui .dialog-ux .modal-ux-content p,.swagger-ui .dialog-ux .modal-ux-header h3,.swagger-ui .errors-wrapper .errors h4,.swagger-ui .errors-wrapper hgroup h4,.swagger-ui .info .base-url,.swagger-ui .info .title,.swagger-ui .info h1,.swagger-ui .info h2,.swagger-ui .info h3,.swagger-ui .info h4,.swagger-ui .info h5,.swagger-ui .info li,.swagger-ui .info p,.swagger-ui .info table,.swagger-ui .loading-container .loading:after,.swagger-ui .model,.swagger-ui .opblock .opblock-section-header h4,.swagger-ui .opblock .opblock-section-header>label,.swagger-ui .opblock-description-wrapper,.swagger-ui .opblock-description-wrapper h4,.swagger-ui .opblock-description-wrapper p,.swagger-ui .opblock-external-docs-wrapper,.swagger-ui .opblock-external-docs-wrapper h4,.swagger-ui .opblock-external-docs-wrapper p,.swagger-ui .opblock-tag,.swagger-ui .opblock-tag small,.swagger-ui .opblock-title_normal,.swagger-ui .opblock-title_normal h4,.swagger-ui .opblock-title_normal p,.swagger-ui .parameter__name,.swagger-ui .parameter__type,.swagger-ui .response-col_links,.swagger-ui .response-col_status,.swagger-ui .responses-inner h4,.swagger-ui .responses-inner h5,.swagger-ui .scheme-container .schemes>label,.swagger-ui .scopes h2,.swagger-ui .servers>label,.swagger-ui .tab li,.swagger-ui label,.swagger-ui section h3,.swagger-ui table.headers td{color:var(--swagger-main-color,#3b4151)}::-webkit-scrollbar{width:16px}::-webkit-scrollbar-thumb{height:56px;border-radius:8px;border:4px solid transparent;background-clip:content-box;background-color:var(--scrollbar-thumb-color,#a5a5a5);transition:background-color .3s}::-webkit-scrollbar-thumb:hover{background-color:var(--scrollbar-thumb-hover-color,#727272)}body::-webkit-scrollbar-thumb{background-color:var(--scrollbar-thumb-color,#a5a5a5)}#theme-switcher-select,.swagger-ui select{background:var(--select-background-icon, #f7f7f7 url('data:image/svg+xml;charset=utf-8,') right 10px center no-repeat)}:focus{outline:0!important}:focus-visible{outline:auto!important;outline-offset:1px}body{background-color:var(--body-background-color,#fafafa);padding-bottom:1rem}*,::after,::before{box-sizing:border-box}.swagger-ui{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;line-height:1.15;margin:0}.swagger-ui .loading-container .loading:before{border:2px solid var(--loading-container-border-color,rgba(85,85,85,.1));border-top-color:var(--loading-container-border-top-color,rgba(0,0,0,.6))}.swagger-ui .errors-wrapper{background:var(--errors-wrapper-background-color,rgba(249,62,62,.1));border:2px solid var(--errors-wrapper-border-color,#f93e3e)}.swagger-ui .errors-wrapper .errors small{color:var(--errors-wrapper-errors-color,#606060)}.swagger-ui .topbar{background-color:transparent;z-index:1000;padding:0}.swagger-ui .topbar.pinned{position:sticky;top:10px}.swagger-ui .topbar.pinned::before{content:'';position:absolute;inset:-10px 0 0;pointer-events:none;user-select:none;backdrop-filter:blur(20px);-webkit-mask-image:linear-gradient(to bottom,black 40%,transparent);mask-image:linear-gradient(to bottom,black 40%,transparent);background:linear-gradient(90deg,rgba(0,0,0,0) 5%,rgba(8,8,8,.4) 50%,rgba(0,0,0,0) 95%)}.swagger-ui .topbar .topbar-wrapper{background-color:var(--topbar-background-color,#000);border:1px solid var(--topbar-border-color,gray);border-top:0;border-radius:0 0 .5rem .5rem;padding:.5rem;box-shadow:0 5px 15px 0 var(--topbar-pinned-shadow-color,rgba(0,0,0,.5))}.swagger-ui .topbar .topbar-wrapper #pin-topbar-btn svg{fill:var(--topbar-pin-icon-color,white)}.swagger-ui .topbar .topbar-wrapper #pin-topbar-btn{cursor:pointer;background:0 0;border:none;margin:.2rem;padding:.2rem;transition:.2s;rotate:z 359deg}.swagger-ui .topbar .topbar-wrapper #pin-topbar-btn:focus-within,.swagger-ui .topbar .topbar-wrapper #pin-topbar-btn:hover{scale:1.05;animation:1.2s infinite pintobbar}#scroll-to-top-btn:active,.swagger-ui .topbar .topbar-wrapper #pin-topbar-btn:active{scale:0.95}.swagger-ui .topbar.pinned .topbar-wrapper{background-color:var(--topbar-pinned-background-color,rgba(0,0,0,.7));border:1px solid var(--topbar-border-color,gray);border-radius:.5rem;backdrop-filter:blur(20px)}#theme-switcher-select,.swagger-ui .topbar .download-url-wrapper .select-label select,.swagger-ui .topbar .download-url-wrapper input[type=text]{border:2px solid var(--topbar-select-border-color,#62a03f)}.swagger-ui .topbar.pinned .topbar-wrapper #pin-topbar-btn{translate:0 5px;rotate:z 0deg}.swagger-ui .topbar.pinned .topbar-wrapper #pin-topbar-btn:focus-within,.swagger-ui .topbar.pinned .topbar-wrapper #pin-topbar-btn:hover{animation:1.2s infinite unpintobbar}@keyframes pintobbar{30%{rotate:z 313deg;translate:0px 1px}40%{translate:0 5px}50%{rotate:z 313deg}}@-moz-keyframes pintobbar{30%{rotate:z 313deg;translate:0px 1px}40%{translate:0 5px}50%{rotate:z 313deg}}@-webkit-keyframes pintobbar{30%{rotate:z 313deg;translate:0px 1px}40%{translate:0 5px}50%{rotate:z 313deg}}@keyframes unpintobbar{10%{translate:0 1px}70%{rotate:z 50deg;translate:-3px 1px}}@-moz-keyframes unpintobbar{10%{translate:0 -4px}70%{rotate:z 50deg;translate:-3px 1px}}@-webkit-keyframes unpintobbar{10%{translate:0 -4px}70%{rotate:z 50deg;translate:-3px 1px}}.swagger-ui .topbar .download-url-wrapper .select-label{color:var(--topbar-select-label-color,#f0f0f0)}.swagger-ui .topbar .download-url-wrapper .download-url-button{background:var(--topbar-download-url-button-background-color,#62a03f);color:var(--topbar-download-url-button-color,#fff)}#theme-switcher-select{min-width:120px;box-shadow:none}.swagger-ui .info a,.swagger-ui .scopes h2 a{color:var(--swagger-info-link,#4990e2)}.swagger-ui .info a:hover{color:var(--swagger-info-link-hover,#1f69c0)}.swagger-ui .info .title small{background:var(--api-version-background-color,#7d8492)}.swagger-ui .info .title small.version-stamp{background-color:var(--api-version-stamp-background-color,#89bf04)}.swagger-ui .info .title small pre{color:var(--api-version-color,#fff)}.swagger-ui .scheme-container{background:0 0;box-shadow:none;margin:0;padding:0}.swagger-ui .auth-wrapper{display:flex;flex:1;justify-content:flex-end;border:1px solid var(--auth-wrapper-border-color,#d5d5d5);padding:0;border-radius:5px;box-shadow:0 3px 5px -5px var(--auth-wrapper-box-shadow-color,#000);background:var(--auth-wrapper-background-color,#fff)}.swagger-ui .auth-container{border-bottom:1px solid var(--auth-container-border-bottom-color,#ebebeb);color:var(--swagger-main-color,#3b4151)}.swagger-ui .auth-container .errors{background-color:var(--auth-container-background-color,#fee);color:var(--auth-container-errors-color,red)}.swagger-ui .btn.authorize{background-color:var(--btn-authorize-background-color,transparent);border-color:var(--btn-authorize-border-color,#49cc90);color:var(--btn-authorize-font-color,#49cc90)}.swagger-ui .btn.authorize svg{fill:var(--btn-authorize-svg-fill-color,#49cc90)}.swagger-ui .opblock-tag-section{display:flex;flex-direction:column;border:1px solid var(--opblock-tag-section-border-color,#ddd);border-radius:.5rem;margin-block:1rem;overflow:clip}.swagger-ui .opblock-tag-section .expand-collapse-all-btn{display:none;user-select:none}.swagger-ui .opblock-tag-section.is-open .expand-collapse-all-btn{background:var(--opblock-tag-section-expandcollapse-background-color,rgb(220 220 220 / 45%));color:var(--swagger-main-color,#3b4151);border:1px solid var(--opblock-tag-section-border-color,#ddd);border-radius:.5rem;padding:5px 10px;font-size:medium;margin-inline:10px;display:block}.swagger-ui .opblock-tag{align-items:center;cursor:pointer;display:flex;padding:10px 20px 10px 10px;color:var(--swagger-main-color,#3b4151);font-size:24px;border-bottom:0 dashed var(--opblock-tag-border-bottom-color,#e1e1e1);margin:0;transition:.2s}.swagger-ui .opblock-tag:focus-within,.swagger-ui .opblock-tag:hover{background:var(--opblock-tag-background-color-hover,rgba(0,0,0,.02))}.swagger-ui .opblock-tag+div .operation-tag-content{margin-inline:5px}#STICKY_OPERATIONS.swagger-ui .opblock-tag-section.is-open .opblock-tag{position:sticky;top:0;z-index:4;margin-bottom:1rem;border-bottom:1px dashed var(--opblock-tag-border-bottom-color,#e1e1e1);background:var(--body-background-color,#fafafa)}#STICKY_OPERATIONS.swagger-ui .opblock-tag-section.is-open .opblock-tag:focus-within,#STICKY_OPERATIONS.swagger-ui .opblock-tag-section.is-open .opblock-tag:hover{background:var(--opblock-tag-pinned-background-color-hover,#f1f1f1)}.swagger-ui:has(> .topbar.pinned) .opblock-tag-section.is-open .opblock-tag{top:4.2rem}.swagger-ui .opblock{border:1px solid var(--opblock-border-color,#000);box-shadow:0 0 3px var(--opblock-shadow-color,rgba(0,0,0,.19))}.swagger-ui .opblock .tab-header .tab-item.active h4 span:after{background:var(--opblock-tabheader-underline-color,gray)}.swagger-ui .opblock .opblock-summary button svg{fill:var(--opblock-summary-svg-icons-color,#000)}.swagger-ui .opblock.is-open .opblock-summary{border-bottom:1px solid var(--opblock-summary-border-bottom-color,#000)}.swagger-ui .opblock.opblock-post,.swagger-ui .opblock.opblock-post .opblock-summary{border-color:var(--opblock-post-border-color,#49cc90)}.swagger-ui .opblock .opblock-section-header{background:var(--opblock-section-header-background-color,hsla(0,0%,100%,.8));box-shadow:0 1px 2px var(--opblock-section-header-shadow-color,rgba(0,0,0,.1))}.swagger-ui .opblock .opblock-summary-method{background:var(--opblock-summary-method-background-color,#000);color:var(--opblock-summary-method-color,#fff);text-shadow:0 1px 0 var(--opblock-summary-method-shadow-color,rgba(0,0,0,.1))}.swagger-ui .opblock .opblock-summary-description,.swagger-ui .opblock .opblock-summary-operation-id,.swagger-ui .opblock .opblock-summary-path,.swagger-ui .opblock .opblock-summary-path__deprecated{color:var(--swagger-main-color,#3b4151);word-break:normal}.swagger-ui .opblock.opblock-post{background:var(--opblock-post-background-color,rgba(73,204,144,.1))}.swagger-ui .opblock.opblock-post .opblock-summary-method{background:var(--opblock-post-method-color,#49cc90)}.swagger-ui .opblock.opblock-put,.swagger-ui .opblock.opblock-put .opblock-summary{border-color:var(--opblock-put-border-color,#fca130)}.swagger-ui .opblock.opblock-post .tab-header .tab-item.active h4 span:after{background:var(--opblock-post-method-color,#49cc90);border-radius:1rem}.swagger-ui .opblock.opblock-put{background:var(--opblock-put-background-color,rgba(252,161,48,.1))}.swagger-ui .opblock.opblock-put .opblock-summary-method,.swagger-ui .opblock.opblock-put .tab-header .tab-item.active h4 span:after{background:var(--opblock-put-method-color,#fca130)}.swagger-ui .opblock.opblock-delete,.swagger-ui .opblock.opblock-delete .opblock-summary{border-color:var(--opblock-delete-border-color,#f93e3e)}.swagger-ui .opblock.opblock-delete{background:var(--opblock-delete-background-color,rgba(249,62,62,.1))}.swagger-ui .opblock.opblock-delete .opblock-summary-method,.swagger-ui .opblock.opblock-delete .tab-header .tab-item.active h4 span:after{background:var(--opblock-delete-method-color,#f93e3e)}.swagger-ui .opblock.opblock-get,.swagger-ui .opblock.opblock-get .opblock-summary{border-color:var(--opblock-get-border-color,#61affe)}.swagger-ui .opblock.opblock-get{background:var(--opblock-get-background-color,rgba(97,175,254,.1))}.swagger-ui .opblock.opblock-get .opblock-summary-method,.swagger-ui .opblock.opblock-get .tab-header .tab-item.active h4 span:after{background:var(--opblock-get-method-color,#61affe)}.swagger-ui .opblock.opblock-patch,.swagger-ui .opblock.opblock-patch .opblock-summary{border-color:var(--opblock-patch-border-color,#50e3c2)}.swagger-ui .opblock.opblock-patch{background:var(--opblock-patch-background-color,rgba(80,227,194,.1))}.swagger-ui .opblock.opblock-patch .opblock-summary-method,.swagger-ui .opblock.opblock-patch .tab-header .tab-item.active h4 span:after{background:var(--opblock-patch-method-color,#50e3c2)}.swagger-ui .opblock.opblock-head,.swagger-ui .opblock.opblock-head .opblock-summary{border-color:var(--opblock-head-border-color,#9012fe)}.swagger-ui .opblock.opblock-head{background:var(--opblock-head-background-color,rgba(144,18,254,.1))}.swagger-ui .opblock.opblock-head .opblock-summary-method,.swagger-ui .opblock.opblock-head .tab-header .tab-item.active h4 span:after{background:var(--opblock-head-method-color,#9012fe)}.swagger-ui .opblock.opblock-options,.swagger-ui .opblock.opblock-options .opblock-summary{border-color:var(--opblock-options-border-color,#0d5aa7)}.swagger-ui .opblock.opblock-options{background:var(--opblock-options-background-color,rgba(13,90,167,.1))}.swagger-ui .opblock.opblock-options .opblock-summary-method,.swagger-ui .opblock.opblock-options .tab-header .tab-item.active h4 span:after{background:var(--opblock-options-method-color,#0d5aa7)}.swagger-ui .opblock.opblock-deprecated,.swagger-ui .opblock.opblock-deprecated .opblock-summary{border-color:var(--opblock-deprecated-border-color,#ebebeb)}.swagger-ui .opblock.opblock-deprecated{background:var(--opblock-deprecated-background-color,hsla(0,0%,92%,.1))}.swagger-ui .opblock.opblock-deprecated .opblock-summary-method,.swagger-ui .opblock.opblock-deprecated .tab-header .tab-item.active h4 span:after{background:var(--opblock-deprecated-method-color,#ebebeb)}.swagger-ui .tab li:first-of-type:after{background:var(--swagger-tabs-divider-color,rgba(0,0,0,.2))}.swagger-ui .response-col_links .response-undocumented,.swagger-ui .response-col_status .response-undocumented{color:var(--response-undocumented-color,#909090)}.swagger-ui .response-control-media-type--accept-controller select{border-color:var(--response-control-media-type-color,green)}.swagger-ui .response-control-media-type__accept-message{color:var(--response-control-media-type-color,green)}.swagger-ui .opblock-body pre.microlight{background:var(--opblock-pre-microlight-background-color,#333)!important;color:var(--opblock-pre-microlight-color,#fff)!important}.swagger-ui .download-contents{background:var(--download-contents-background-color,#7d8293);color:var(--download-contents-color,#fff)}.swagger-ui .copy-to-clipboard{background:var(--copy-to-clipboard-background-color,#7d8293)}.swagger-ui .copy-to-clipboard button{background:var(--copy-to-clipboard-icon, url('data:image/svg+xml;charset=utf-8,') 50% no-repeat)}.swagger-ui .filter .operation-filter-input{border:2px solid var(--filter-operation-filter-input-border-color,#d8dde7)}.swagger-ui .download-url-wrapper .failed,.swagger-ui .filter .failed{color:var(--filter-download-failed-color,red)}.swagger-ui .download-url-wrapper .loading,.swagger-ui .filter .loading{color:var(--filter-download-loading-color,#aaa)}.swagger-ui table.headers .header-example{color:var(--table-headers-example-color,#999)}.swagger-ui table thead tr td,.swagger-ui table thead tr th{border-bottom:1px solid var(--table-thead-border-bottom-color,rgba(59,65,81,.2));color:var(--swagger-main-color,#3b4151)}.swagger-ui .parameter__name.required span{color:var(--parameter-name-required-symbol-color,red)}.swagger-ui .parameter__name.required::after{color:var(--parameter-name-required-color,rgba(255,0,0,.6))}.swagger-ui .parameter__extension,.swagger-ui .parameter__in,.swagger-ui .response__extension{color:var(--parameter-in-extension-color,gray)}.swagger-ui .parameter__deprecated{color:var(--parameter-deprecated-color,red)}.swagger-ui .btn,.swagger-ui select,.swagger-ui textarea{color:var(--swagger-main-color,#3b4151)}.swagger-ui .btn{background:var(--button-background-color,transparent);border:2px solid var(--button-border-color,gray);box-shadow:0 1px 2px var(--button-shadow-color,rgba(0,0,0,.1))}.swagger-ui .btn:hover{box-shadow:0 0 5px var(--button-shadow-hover-color,rgba(0,0,0,.3))}.swagger-ui .btn.cancel{background-color:var(--button-cancel-background-color,transparent);border-color:var(--button-cancel-border-color,#ff6060);color:var(--button-cancel-color,#ff6060)}.swagger-ui .btn.execute{background-color:var(--button-execute-background-color,#4990e2);border-color:var(--button-execute-border-color,#4990e2);color:var(--button-execute-color,#fff)}.swagger-ui button.invalid{background:var(--button-invalid-background-color,#feebeb);border-color:var(--button-invalid-border-color,#f93e3e)}.swagger-ui .expand-methods svg,.swagger-ui .expand-operation svg{fill:var(--expand-operation-svg-arrow-color,#707070)}.swagger-ui .expand-methods:hover svg,.swagger-ui .expand-operation:hover svg{fill:var(--expand-operation-svg-arrow-hover-color,#404040)}.swagger-ui select{border:2px solid var(--select-border-color,#41444e);box-shadow:0 1px 2px 0 var(--select-shadow-color,rgba(0,0,0,.25));cursor:pointer}.swagger-ui select[multiple]{background:var(--select-background-color,#f7f7f7)}.swagger-ui select:disabled{opacity:.5;cursor:not-allowed}.swagger-ui input[type=email],.swagger-ui input[type=file],.swagger-ui input[type=password],.swagger-ui input[type=search],.swagger-ui input[type=text],.swagger-ui textarea{background:var(--input-background-color,#fff);border:1px solid var(--input-border-color,#d9d9d9);color:var(--swagger-main-color,#3b4151)}.swagger-ui input[type=email].invalid,.swagger-ui input[type=file].invalid,.swagger-ui input[type=password].invalid,.swagger-ui input[type=search].invalid,.swagger-ui input[type=text].invalid,.swagger-ui select.invalid,.swagger-ui textarea.invalid{background:var(--input-invalid-background-color,#feebeb);border-color:var(--input-invalid-border-color,#f93e3e);color:var(--input-invalid-color,#3b4151)}.swagger-ui input[disabled],.swagger-ui select[disabled]{background-color:var(--input-disabled-background-color,#fafafa);color:var(--input-disabled-color,#888)}.swagger-ui select[disabled]{border-color:var(--select-disabled-border-color,#888)}.swagger-ui textarea[disabled]{background-color:var(--textarea-disabled-background-color,#41444e);color:var(--textarea-disabled-color,#fff)}.swagger-ui textarea{background:var(--textarea-background-color,hsla(0,0%,100%,.8))}.swagger-ui textarea:focus{border:2px solid var(--textarea-border-focus-color,#61affe)}.swagger-ui textarea.curl{background:var(--textarea-curl-background-color,#41444e);color:var(--textarea-curl-color,#fff)}.swagger-ui .checkbox{color:var(--checkbox-color,#303030)}.swagger-ui .checkbox input[type=checkbox]+label>.item{background:var(--checkbox-label-item-background-color,#e8e8e8);box-shadow:0 0 0 2px var(--checkbox-label-item-shadow-color,#e8e8e8)}.swagger-ui .checkbox input[type=checkbox]:checked+label>.item{background:var(--checkbox-label-item-icon, #e8e8e8 url('data:image/svg+xml;charset=utf-8,') 50% no-repeat)}.swagger-ui .dialog-ux .backdrop-ux{background:var(--dialog-backdrop-color,rgba(0,0,0,.8))}.swagger-ui .dialog-ux .modal-ux{background:var(--dialog-background-color,#fff);border:1px solid var(--dialog-border-color,#ebebeb);box-shadow:0 10px 30px 0 var(--dialog-shadow-color,rgba(0,0,0,.2))}.swagger-ui .dialog-ux .modal-ux-header{border-bottom:1px solid var(--dialog-border-color,#ebebeb)}.swagger-ui .dialog-ux .modal-ux-header button svg{fill:var(--dialog-close-button-icon-color,black)}.swagger-ui .model .deprecated span,.swagger-ui .model .deprecated td{color:var(--model-deprecated-color,#a0a0a0)!important}.swagger-ui .model-toggle{cursor:pointer;-webkit-transition:-webkit-transform .15s ease-in;transition:transform .15s ease-in;transition:transform .15s ease-in,-webkit-transform .15s ease-in;-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-transform-origin:50% 50%;transform-origin:50% 50%}.swagger-ui .model-toggle.collapsed{-webkit-transform:rotate(0);transform:rotate(0)}.swagger-ui .model-toggle::after{background:var(--model-arrow-icon, url('data:image/svg+xml;charset=utf-8,') 50% no-repeat)}.swagger-ui .model-hint{background:var(--model-hint-background-color,rgba(0,0,0,.7));color:var(--model-hint-color,#ebebeb)}.swagger-ui .model .property{color:var(--model-property-color,#999)}.swagger-ui .model .property.primitive{color:var(--model-property-primitive-color,#6b6b6b)}.swagger-ui table.model tr.property-row .star{color:var(--model-property-required-symbol-color,red)}.swagger-ui .model .external-docs,.swagger-ui table.model tr.description{color:var(--model-property-description,#666)}.swagger-ui table.model tr.extension{color:var(--model-property-extension,#777)}.swagger-ui section.models{border:1px solid var(--model-section-border-color,rgba(59,65,81,.3));border-radius:.5rem;padding:0;margin:0}.swagger-ui section.models.is-open h4{border-bottom:1px solid var(--model-section-border-color,rgba(59,65,81,.3))}.swagger-ui section.models h4{color:var(--model-section-header-color,#606060);padding:0}.swagger-ui section.models h4:hover{background:var(--model-section-header-hover-background-color,rgba(0,0,0,.02))}.swagger-ui section.models h4 svg{fill:var(--expand-model-svg-arrow-color,#707070)}.swagger-ui section.models h4 button.models-control{color:var(--model-section-header-color,#606060);padding:20px}.swagger-ui section.models h5{color:var(--model-section-little-header-color,#707070)}.swagger-ui section.models .model-container{background:var(--model-container-background-color,rgba(0,0,0,.05))}.swagger-ui section.models .model-container:hover{background:var(--model-container-hover-background-color,rgba(0,0,0,.07))}.swagger-ui .model-box{background:var(--model-box-background-color,rgba(0,0,0,.1))}.swagger-ui .model-title{color:var(--model-title-color,#505050)}.swagger-ui .model-deprecated-warning{color:var(--model-deprecated-warning-color,#f93e3e)}.swagger-ui .prop-type{color:var(--model-prop-type-color,#55a)}.swagger-ui .prop-format{color:var(--model-prop-format-color,#606060)}.swagger-ui .markdown pre,.swagger-ui .renderedMarkdown pre{color:var(--rendered-markdown-pre-color,#000)}.swagger-ui .markdown code,.swagger-ui .renderedMarkdown code{background:var(--rendered-markdown-code-background-color,rgba(0,0,0,.05));color:var(--rendered-markdown-code-color,#9012fe)}.scroll-to-top-wrapper{position:fixed;bottom:0;left:0;right:0;text-align:end;box-sizing:border-box;margin:0 auto;max-width:1460px;padding:0 20px;width:100%;pointer-events:none;z-index:1000}#scroll-to-top-btn{background-color:var(--scroll-to-top-button-background-color,#000000d1);border:1px solid var(--scroll-to-top-button-border-color,#d1c9c9);border-radius:50%;color:#fff;cursor:pointer;width:2.5rem;aspect-ratio:1;backdrop-filter:blur(20px);margin:10px;position:relative;transition:background-color .5s,opacity .6s .2s,translate .8s,scale .2s;opacity:0;translate:0 100px;box-shadow:0 0 10px 0 var(--scroll-to-top-button-shadow-color,rgba(0,0,0,.4))}#scroll-to-top-btn.showBtn{opacity:1;translate:0 0;pointer-events:auto}#scroll-to-top-btn::before{position:absolute;content:'';top:10px;left:12px;right:12px;background-color:var(--scroll-to-top-button-icon-color,#fff);border-radius:1rem;height:2px}#scroll-to-top-btn::after{position:absolute;content:'';top:15px;left:50%;border:solid var(--scroll-to-top-button-icon-color,#fff);border-width:0 2px 2px 0;padding:3px;transform:rotate(-135deg);translate:-50% 4px}#scroll-to-top-btn:focus-within,#scroll-to-top-btn:hover{background-color:var(--scroll-to-top-button-hover-background-color,#0d0d0dba)}#scroll-to-top-btn:focus-within::after,#scroll-to-top-btn:hover::after{animation:1.2s ease-in infinite bounceUp}@keyframes bounceUp{100%{translate:-50% 4px}25%{translate:-50% 6px}60%{translate:-50% -2px}}@-moz-keyframes bounceUp{100%{translate:-50% 4px}25%{translate:-50% 6px}60%{translate:-50% -2px}}@-webkit-keyframes bounceUp{100%{translate:-50% 4px}25%{translate:-50% 6px}60%{translate:-50% -2px}}@media only screen and (max-width:680px){.swagger-ui .opblock .opblock-summary-description,.swagger-ui .opblock .opblock-summary-operation-id,.swagger-ui .opblock .opblock-summary-path,.swagger-ui .opblock .opblock-summary-path__deprecated{word-break:break-all}}
\ No newline at end of file
+/*Common Theme https://github.com/teociaps/SwaggerUI.Themes */@charset "UTF-8";:root{--topbar-pinned-height:4rem;--filterbar-pinned-height:5rem}::-webkit-scrollbar{width:16px}::-webkit-scrollbar-thumb{height:56px;border-radius:8px;border:4px solid transparent;background-clip:content-box;background-color:var(--scrollbar-thumb-color,#a5a5a5);transition:background-color .3s}::-webkit-scrollbar-thumb:hover{background-color:var(--scrollbar-thumb-hover-color,#727272)}body::-webkit-scrollbar-thumb{background-color:var(--scrollbar-thumb-color,#a5a5a5)}*{font-family:system-ui,sans-serif}:focus{font-family:system-ui,sans-serif;outline:0!important}:focus-visible{outline:auto!important;outline-offset:1px}body{background-color:var(--body-background-color,#fafafa);padding-bottom:1rem}*,::after,::before{box-sizing:border-box}.swagger-ui{color:var(--swagger-main-color,#3b4151);-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;line-height:1.15;margin:0}.swagger-ui .loading-container .loading:after{color:var(--swagger-main-color,#3b4151)}.swagger-ui .loading-container .loading:before{border:2px solid var(--loading-container-border-color,rgba(85,85,85,.1));border-top-color:var(--loading-container-border-top-color,rgba(0,0,0,.6))}.swagger-ui .errors-wrapper{background:var(--errors-wrapper-background-color,rgba(249,62,62,.1));border:2px solid var(--errors-wrapper-border-color,#f93e3e)}.swagger-ui .errors-wrapper .errors h4,.swagger-ui .errors-wrapper hgroup h4{color:var(--swagger-main-color,#3b4151)}.swagger-ui .errors-wrapper .errors small{color:var(--errors-wrapper-errors-color,#606060)}.swagger-ui .topbar{background-color:transparent;z-index:1000;padding:0}.swagger-ui .topbar.pinned{position:sticky;top:10px;--pinned-topbar-offset:var(--topbar-pinned-height)}.swagger-ui:has(> .topbar.pinned){--pinned-topbar-offset:var(--topbar-pinned-height)}.swagger-ui .topbar.pinned::before{content:'';position:absolute;inset:-10px 0 0;pointer-events:none;user-select:none;backdrop-filter:blur(20px);-webkit-mask-image:linear-gradient(to bottom,black 40%,transparent);mask-image:linear-gradient(to bottom,black 40%,transparent);background:linear-gradient(90deg,rgba(0,0,0,0) 5%,rgba(8,8,8,.4) 50%,rgba(0,0,0,0) 95%)}.swagger-ui .topbar .topbar-wrapper{background-color:var(--topbar-background-color,#000);border:1px solid var(--topbar-border-color,gray);border-top:0;border-radius:0 0 .5rem .5rem;padding:.5rem;box-shadow:0 5px 15px 0 var(--topbar-pinned-shadow-color,rgba(0,0,0,.5))}.swagger-ui .topbar .topbar-wrapper #pin-topbar-btn svg{fill:var(--topbar-pin-icon-color,white)}.swagger-ui .filter #pin-filterbar-btn,.swagger-ui .topbar .topbar-wrapper #pin-topbar-btn{cursor:pointer;background:0 0;border:none;margin:.2rem;padding:.2rem;transition:all .2s;rotate:z 359deg}.swagger-ui .filter #pin-filterbar-btn:focus-within,.swagger-ui .filter #pin-filterbar-btn:hover,.swagger-ui .topbar .topbar-wrapper #pin-topbar-btn:focus-within,.swagger-ui .topbar .topbar-wrapper #pin-topbar-btn:hover{scale:1.05;animation:pintobbar 1.2s infinite}.swagger-ui .filter #pin-filterbar-btn:active,.swagger-ui .topbar .topbar-wrapper #pin-topbar-btn:active{scale:0.95}.swagger-ui .topbar.pinned .topbar-wrapper{background-color:var(--topbar-pinned-background-color,rgba(0,0,0,.7));border:1px solid var(--topbar-border-color,gray);border-radius:.5rem;backdrop-filter:blur(20px)}.swagger-ui .filter.pinned #pin-filterbar-btn,.swagger-ui .topbar.pinned .topbar-wrapper #pin-topbar-btn{rotate:z 0deg}.swagger-ui .topbar.pinned .topbar-wrapper #pin-topbar-btn{translate:0 5px}.swagger-ui .filter.pinned #pin-filterbar-btn:focus-within,.swagger-ui .filter.pinned #pin-filterbar-btn:hover,.swagger-ui .topbar.pinned .topbar-wrapper #pin-topbar-btn:focus-within,.swagger-ui .topbar.pinned .topbar-wrapper #pin-topbar-btn:hover{animation:unpintobbar 1.2s infinite}@keyframes pintobbar{30%{rotate:z 313deg;translate:0px 1px}40%{translate:0 5px}50%{rotate:z 313deg}}@-moz-keyframes pintobbar{30%{rotate:z 313deg;translate:0px 1px}40%{translate:0 5px}50%{rotate:z 313deg}}@-webkit-keyframes pintobbar{30%{rotate:z 313deg;translate:0px 1px}40%{translate:0 5px}50%{rotate:z 313deg}}@keyframes unpintobbar{10%{translate:0 1px}70%{rotate:z 50deg;translate:-3px 1px}}@-moz-keyframes unpintobbar{10%{translate:0 -4px}70%{rotate:z 50deg;translate:-3px 1px}}@-webkit-keyframes unpintobbar{10%{translate:0 -4px}70%{rotate:z 50deg;translate:-3px 1px}}.swagger-ui .topbar .download-url-wrapper .select-label select,.swagger-ui .topbar .download-url-wrapper input[type=text]{border:2px solid var(--topbar-select-border-color,#62a03f)}.swagger-ui .topbar .download-url-wrapper .select-label{color:var(--topbar-select-label-color,#f0f0f0)}.swagger-ui .topbar .download-url-wrapper .download-url-button{background:var(--topbar-download-url-button-background-color,#62a03f);color:var(--topbar-download-url-button-color,#fff)}.swagger-ui .topbar .dark-mode-toggle{display:none}#theme-switcher-select{background:var(--select-background-icon, #f7f7f7 url('data:image/svg+xml;charset=utf-8,') right 10px center no-repeat);border:2px solid var(--topbar-select-border-color,#62a03f);min-width:120px;box-shadow:none}.swagger-ui .info .base-url,.swagger-ui .info .title,.swagger-ui .info h1,.swagger-ui .info h2,.swagger-ui .info h3,.swagger-ui .info h4,.swagger-ui .info h5,.swagger-ui .info li,.swagger-ui .info p,.swagger-ui .info table{color:var(--swagger-main-color,#3b4151)}.swagger-ui .info a{color:var(--swagger-info-link,#4990e2)}.swagger-ui .info a:hover{color:var(--swagger-info-link-hover,#1f69c0)}.swagger-ui .info .title small{background:var(--api-version-background-color,#7d8492)}.swagger-ui .info .title small.version-stamp{background-color:var(--api-version-stamp-background-color,#89bf04)}.swagger-ui .info .title small pre{color:var(--api-version-color,#fff)}.swagger-ui .scheme-container{background:0 0;box-shadow:none;margin:0;padding:0}.swagger-ui .auth-wrapper{display:flex;flex:1;justify-content:flex-end;border:1px solid var(--auth-wrapper-border-color,#d5d5d5);padding:0;border-radius:5px;box-shadow:0 3px 5px -5px var(--auth-wrapper-box-shadow-color,#000);background:var(--auth-wrapper-background-color,#fff)}.swagger-ui .scheme-container .schemes>label{color:var(--swagger-main-color,#3b4151)}.swagger-ui .auth-container{border-bottom:1px solid var(--auth-container-border-bottom-color,#ebebeb);color:var(--swagger-main-color,#3b4151)}.swagger-ui .auth-container .errors{background-color:var(--auth-container-background-color,#fee);color:var(--auth-container-errors-color,red)}.swagger-ui .btn.authorize{background-color:var(--btn-authorize-background-color,transparent);border-color:var(--btn-authorize-border-color,#49cc90);color:var(--btn-authorize-font-color,#49cc90)}.swagger-ui .btn.authorize svg{fill:var(--btn-authorize-svg-fill-color,#49cc90)}.swagger-ui .opblock-tag-section{display:flex;flex-direction:column;border:1px solid var(--opblock-tag-section-border-color,#ddd);border-radius:.5rem;margin-block:1rem;overflow:clip}.swagger-ui .opblock-tag-section .expand-collapse-all-btn{display:none;user-select:none}.swagger-ui .opblock-tag-section.is-open .expand-collapse-all-btn{background:var(--opblock-tag-section-expandcollapse-background-color,rgb(220 220 220 / 45%));color:var(--swagger-main-color,#3b4151);border:1px solid var(--opblock-tag-section-border-color,#ddd);border-radius:.5rem;padding:5px 10px;font-size:medium;margin-inline:10px;display:block}.swagger-ui .opblock-tag,.swagger-ui .opblock-tag small,.swagger-ui .parameter__type{color:var(--swagger-main-color,#3b4151)}.swagger-ui .opblock-tag{align-items:center;cursor:pointer;display:flex;padding:10px 20px 10px 10px;color:var(--swagger-main-color,#3b4151);font-size:24px;border-bottom:0 dashed var(--opblock-tag-border-bottom-color,#e1e1e1);margin:0;transition:all .2s}.swagger-ui .opblock-tag:focus-within,.swagger-ui .opblock-tag:hover{background:var(--opblock-tag-background-color-hover,rgba(0,0,0,.02))}.swagger-ui .opblock-tag+div .operation-tag-content{margin-inline:5px}#STICKY_OPERATIONS.swagger-ui .opblock-tag-section.is-open .opblock-tag{position:sticky;top:0;z-index:4;margin-bottom:1rem;border-bottom:1px dashed var(--opblock-tag-border-bottom-color,#e1e1e1);background:var(--body-background-color,#fafafa)}#STICKY_OPERATIONS.swagger-ui .opblock-tag-section.is-open .opblock-tag:focus-within,#STICKY_OPERATIONS.swagger-ui .opblock-tag-section.is-open .opblock-tag:hover{background:var(--opblock-tag-pinned-background-color-hover,#f1f1f1)}.swagger-ui .opblock-tag-section.is-open .opblock-tag{top:calc(var(--pinned-topbar-offset,0px) + var(--pinned-filter-offset,0px))}.swagger-ui .opblock{border:1px solid var(--opblock-border-color,#000);box-shadow:0 0 3px var(--opblock-shadow-color,rgba(0,0,0,.19))}.swagger-ui .opblock .tab-header .tab-item.active h4 span:after{background:var(--opblock-tabheader-underline-color,gray)}.swagger-ui .opblock .opblock-summary button svg{fill:var(--opblock-summary-svg-icons-color,#000)}.swagger-ui .opblock.is-open .opblock-summary{border-bottom:1px solid var(--opblock-summary-border-bottom-color,#000)}.swagger-ui .opblock .opblock-section-header{background:var(--opblock-section-header-background-color,hsla(0,0%,100%,.8));box-shadow:0 1px 2px var(--opblock-section-header-shadow-color,rgba(0,0,0,.1))}.swagger-ui .opblock .opblock-section-header h4,.swagger-ui .opblock .opblock-section-header>label{color:var(--swagger-main-color,#3b4151)}.swagger-ui .opblock .opblock-summary-method{background:var(--opblock-summary-method-background-color,#000);color:var(--opblock-summary-method-color,#fff);text-shadow:0 1px 0 var(--opblock-summary-method-shadow-color,rgba(0,0,0,.1))}.swagger-ui .opblock .opblock-summary-description,.swagger-ui .opblock .opblock-summary-operation-id,.swagger-ui .opblock .opblock-summary-path,.swagger-ui .opblock .opblock-summary-path__deprecated{color:var(--swagger-main-color,#3b4151);word-break:normal}.swagger-ui .opblock.opblock-post{background:var(--opblock-post-background-color,rgba(73,204,144,.1));border-color:var(--opblock-post-border-color,#49cc90)}.swagger-ui .opblock.opblock-post .opblock-summary-method{background:var(--opblock-post-method-color,#49cc90)}.swagger-ui .opblock.opblock-post .opblock-summary{border-color:var(--opblock-post-border-color,#49cc90)}.swagger-ui .opblock.opblock-post .tab-header .tab-item.active h4 span:after{background:var(--opblock-post-method-color,#49cc90);border-radius:1rem}.swagger-ui .opblock.opblock-put{background:var(--opblock-put-background-color,rgba(252,161,48,.1));border-color:var(--opblock-put-border-color,#fca130)}.swagger-ui .opblock.opblock-put .opblock-summary-method{background:var(--opblock-put-method-color,#fca130)}.swagger-ui .opblock.opblock-put .opblock-summary{border-color:var(--opblock-put-border-color,#fca130)}.swagger-ui .opblock.opblock-put .tab-header .tab-item.active h4 span:after{background:var(--opblock-put-method-color,#fca130)}.swagger-ui .opblock.opblock-delete{background:var(--opblock-delete-background-color,rgba(249,62,62,.1));border-color:var(--opblock-delete-border-color,#f93e3e)}.swagger-ui .opblock.opblock-delete .opblock-summary-method{background:var(--opblock-delete-method-color,#f93e3e)}.swagger-ui .opblock.opblock-delete .opblock-summary{border-color:var(--opblock-delete-border-color,#f93e3e)}.swagger-ui .opblock.opblock-delete .tab-header .tab-item.active h4 span:after{background:var(--opblock-delete-method-color,#f93e3e)}.swagger-ui .opblock.opblock-get{background:var(--opblock-get-background-color,rgba(97,175,254,.1));border-color:var(--opblock-get-border-color,#61affe)}.swagger-ui .opblock.opblock-get .opblock-summary-method{background:var(--opblock-get-method-color,#61affe)}.swagger-ui .opblock.opblock-get .opblock-summary{border-color:var(--opblock-get-border-color,#61affe)}.swagger-ui .opblock.opblock-get .tab-header .tab-item.active h4 span:after{background:var(--opblock-get-method-color,#61affe)}.swagger-ui .opblock.opblock-patch{background:var(--opblock-patch-background-color,rgba(80,227,194,.1));border-color:var(--opblock-patch-border-color,#50e3c2)}.swagger-ui .opblock.opblock-patch .opblock-summary-method{background:var(--opblock-patch-method-color,#50e3c2)}.swagger-ui .opblock.opblock-patch .opblock-summary{border-color:var(--opblock-patch-border-color,#50e3c2)}.swagger-ui .opblock.opblock-patch .tab-header .tab-item.active h4 span:after{background:var(--opblock-patch-method-color,#50e3c2)}.swagger-ui .opblock.opblock-head{background:var(--opblock-head-background-color,rgba(144,18,254,.1));border-color:var(--opblock-head-border-color,#9012fe)}.swagger-ui .opblock.opblock-head .opblock-summary-method{background:var(--opblock-head-method-color,#9012fe)}.swagger-ui .opblock.opblock-head .opblock-summary{border-color:var(--opblock-head-border-color,#9012fe)}.swagger-ui .opblock.opblock-head .tab-header .tab-item.active h4 span:after{background:var(--opblock-head-method-color,#9012fe)}.swagger-ui .opblock.opblock-options{background:var(--opblock-options-background-color,rgba(13,90,167,.1));border-color:var(--opblock-options-border-color,#0d5aa7)}.swagger-ui .opblock.opblock-options .opblock-summary-method{background:var(--opblock-options-method-color,#0d5aa7)}.swagger-ui .opblock.opblock-options .opblock-summary{border-color:var(--opblock-options-border-color,#0d5aa7)}.swagger-ui .opblock.opblock-options .tab-header .tab-item.active h4 span:after{background:var(--opblock-options-method-color,#0d5aa7)}.swagger-ui .opblock.opblock-deprecated{background:var(--opblock-deprecated-background-color,hsla(0,0%,92%,.1));border-color:var(--opblock-deprecated-border-color,#ebebeb)}.swagger-ui .opblock.opblock-deprecated .opblock-summary-method{background:var(--opblock-deprecated-method-color,#ebebeb)}.swagger-ui .opblock.opblock-deprecated .opblock-summary{border-color:var(--opblock-deprecated-border-color,#ebebeb)}.swagger-ui .opblock.opblock-deprecated .tab-header .tab-item.active h4 span:after{background:var(--opblock-deprecated-method-color,#ebebeb)}.swagger-ui .tab li{color:var(--swagger-main-color,#3b4151)}.swagger-ui .tab li:first-of-type:after{background:var(--swagger-tabs-divider-color,rgba(0,0,0,.2))}.swagger-ui .opblock-description-wrapper,.swagger-ui .opblock-description-wrapper h4,.swagger-ui .opblock-description-wrapper p,.swagger-ui .opblock-external-docs-wrapper,.swagger-ui .opblock-external-docs-wrapper h4,.swagger-ui .opblock-external-docs-wrapper p,.swagger-ui .opblock-title_normal,.swagger-ui .opblock-title_normal h4,.swagger-ui .opblock-title_normal p,.swagger-ui .response-col_links,.swagger-ui .response-col_status,.swagger-ui .responses-inner h4,.swagger-ui .responses-inner h5{color:var(--swagger-main-color,#3b4151)}.swagger-ui .response-col_links .response-undocumented,.swagger-ui .response-col_status .response-undocumented{color:var(--response-undocumented-color,#909090)}.swagger-ui .response-control-media-type--accept-controller select{border-color:var(--response-control-media-type-color,green)}.swagger-ui .response-control-media-type__accept-message{color:var(--response-control-media-type-color,green)}.swagger-ui .opblock-body pre.microlight{background:var(--opblock-pre-microlight-background-color,#333)!important;color:var(--opblock-pre-microlight-color,#fff)!important}.swagger-ui .download-contents{background:var(--download-contents-background-color,#7d8293);color:var(--download-contents-color,#fff)}.swagger-ui .copy-to-clipboard{background:var(--copy-to-clipboard-background-color,#7d8293)}.swagger-ui .copy-to-clipboard button{background:var(--copy-to-clipboard-icon, url('data:image/svg+xml;charset=utf-8,') 50% no-repeat)}.swagger-ui .filter{position:relative;display:flex;align-items:center}.swagger-ui .filter .filter-container{flex:1;min-width:0}.swagger-ui .filter #pin-filterbar-btn{position:absolute;right:1.5rem}.swagger-ui .filter .operation-filter-input{border:2px solid var(--filter-operation-filter-input-border-color,#d8dde7)}.swagger-ui .filter.pinned.stuck .operation-filter-input{box-shadow:0 19px 0 0 var(--body-background-color,#fafafa),0 20px 0 0 var(--opblock-tag-section-border-color,#ddd)}.swagger-ui div:has(> .filter-container > .filter.pinned){position:sticky;top:var(--pinned-topbar-offset,0);z-index:5;background:var(--body-background-color,#fafafa);--pinned-filter-offset:var(--filterbar-pinned-height)}.swagger-ui:has(.filter.pinned){--pinned-filter-offset:var(--filterbar-pinned-height)}.swagger-ui .filter #pin-filterbar-btn svg{fill:var(--filterbar-pin-icon-color,#707070)}.swagger-ui .download-url-wrapper .failed,.swagger-ui .filter .failed{color:var(--filter-download-failed-color,red)}.swagger-ui .download-url-wrapper .loading,.swagger-ui .filter .loading{color:var(--filter-download-loading-color,#aaa)}.swagger-ui section h3{color:var(--swagger-main-color,#3b4151)}.swagger-ui .servers>label,.swagger-ui table.headers td{color:var(--swagger-main-color,#3b4151)}.swagger-ui table.headers .header-example{color:var(--table-headers-example-color,#999)}.swagger-ui table thead tr td,.swagger-ui table thead tr th{border-bottom:1px solid var(--table-thead-border-bottom-color,rgba(59,65,81,.2));color:var(--swagger-main-color,#3b4151)}.swagger-ui .parameter__name{color:var(--swagger-main-color,#3b4151)}.swagger-ui .parameter__name.required span{color:var(--parameter-name-required-symbol-color,red)}.swagger-ui .parameter__name.required::after{color:var(--parameter-name-required-color,rgba(255,0,0,.6))}.swagger-ui .parameter__extension,.swagger-ui .parameter__in,.swagger-ui .response__extension{color:var(--parameter-in-extension-color,gray)}.swagger-ui .parameter__deprecated{color:var(--parameter-deprecated-color,red)}.swagger-ui .btn{background:var(--button-background-color,transparent);border:2px solid var(--button-border-color,gray);box-shadow:0 1px 2px var(--button-shadow-color,rgba(0,0,0,.1));color:var(--swagger-main-color,#3b4151)}.swagger-ui .btn:hover{box-shadow:0 0 5px var(--button-shadow-hover-color,rgba(0,0,0,.3))}.swagger-ui .btn.cancel{background-color:var(--button-cancel-background-color,transparent);border-color:var(--button-cancel-border-color,#ff6060);color:var(--button-cancel-color,#ff6060)}.swagger-ui .btn.execute{background-color:var(--button-execute-background-color,#4990e2);border-color:var(--button-execute-border-color,#4990e2);color:var(--button-execute-color,#fff)}.swagger-ui button.invalid{background:var(--button-invalid-background-color,#feebeb);border-color:var(--button-invalid-border-color,#f93e3e)}.swagger-ui .expand-methods svg,.swagger-ui .expand-operation svg{fill:var(--expand-operation-svg-arrow-color,#707070)}.swagger-ui .expand-methods:hover svg,.swagger-ui .expand-operation:hover svg{fill:var(--expand-operation-svg-arrow-hover-color,#404040)}.swagger-ui select{background:var(--select-background-icon, #f7f7f7 url('data:image/svg+xml;charset=utf-8,') right 10px center no-repeat);border:2px solid var(--select-border-color,#41444e);box-shadow:0 1px 2px 0 var(--select-shadow-color,rgba(0,0,0,.25));color:var(--swagger-main-color,#3b4151);cursor:pointer}.swagger-ui select[multiple]{background:var(--select-background-color,#f7f7f7)}.swagger-ui select:disabled{opacity:.5;cursor:not-allowed}.swagger-ui label{color:var(--swagger-main-color,#3b4151)}.swagger-ui input[type=email],.swagger-ui input[type=file],.swagger-ui input[type=password],.swagger-ui input[type=search],.swagger-ui input[type=text],.swagger-ui textarea{background:var(--input-background-color,#fff);border:1px solid var(--input-border-color,#d9d9d9);color:var(--swagger-main-color,#3b4151)}.swagger-ui input[type=email].invalid,.swagger-ui input[type=file].invalid,.swagger-ui input[type=password].invalid,.swagger-ui input[type=search].invalid,.swagger-ui input[type=text].invalid,.swagger-ui select.invalid,.swagger-ui textarea.invalid{background:var(--input-invalid-background-color,#feebeb);border-color:var(--input-invalid-border-color,#f93e3e);color:var(--input-invalid-color,#3b4151)}.swagger-ui input[disabled],.swagger-ui select[disabled]{background-color:var(--input-disabled-background-color,#fafafa);color:var(--input-disabled-color,#888)}.swagger-ui select[disabled]{border-color:var(--select-disabled-border-color,#888)}.swagger-ui textarea[disabled]{background-color:var(--textarea-disabled-background-color,#41444e);color:var(--textarea-disabled-color,#fff)}.swagger-ui textarea{background:var(--textarea-background-color,hsla(0,0%,100%,.8));color:var(--swagger-main-color,#3b4151)}.swagger-ui textarea:focus{border:2px solid var(--textarea-border-focus-color,#61affe)}.swagger-ui textarea.curl{background:var(--textarea-curl-background-color,#41444e);color:var(--textarea-curl-color,#fff)}.swagger-ui .checkbox{color:var(--checkbox-color,#303030)}.swagger-ui .checkbox p{color:var(--swagger-main-color,#3b4151)}.swagger-ui .checkbox input[type=checkbox]+label>.item{background:var(--checkbox-label-item-background-color,#e8e8e8);box-shadow:0 0 0 2px var(--checkbox-label-item-shadow-color,#e8e8e8)}.swagger-ui .checkbox input[type=checkbox]:checked+label>.item{background:var(--checkbox-label-item-icon, #e8e8e8 url('data:image/svg+xml;charset=utf-8,') 50% no-repeat)}.swagger-ui .dialog-ux .backdrop-ux{background:var(--dialog-backdrop-color,rgba(0,0,0,.8))}.swagger-ui .dialog-ux .modal-ux{background:var(--dialog-background-color,#fff);border:1px solid var(--dialog-border-color,#ebebeb);box-shadow:0 10px 30px 0 var(--dialog-shadow-color,rgba(0,0,0,.2))}.swagger-ui .dialog-ux .modal-ux-content h4,.swagger-ui .dialog-ux .modal-ux-content p,.swagger-ui .dialog-ux .modal-ux-header h3{color:var(--swagger-main-color,#3b4151)}.swagger-ui .dialog-ux .modal-ux-header{border-bottom:1px solid var(--dialog-border-color,#ebebeb)}.swagger-ui .dialog-ux .modal-ux-header button svg{fill:var(--dialog-close-button-icon-color,black)}.swagger-ui .model{color:var(--swagger-main-color,#3b4151)}.swagger-ui .model .deprecated span,.swagger-ui .model .deprecated td{color:var(--model-deprecated-color,#a0a0a0)!important}.swagger-ui .model-toggle{cursor:pointer;-webkit-transition:-webkit-transform .15s ease-in;transition:-webkit-transform .15s ease-in;transition:transform .15s ease-in;transition:transform .15s ease-in,-webkit-transform .15s ease-in;-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-transform-origin:50% 50%;transform-origin:50% 50%}.swagger-ui .model-toggle.collapsed{-webkit-transform:rotate(0);transform:rotate(0)}.swagger-ui .model-toggle::after{background:var(--model-arrow-icon, url('data:image/svg+xml;charset=utf-8,') 50% no-repeat)}.swagger-ui .model-hint{background:var(--model-hint-background-color,rgba(0,0,0,.7));color:var(--model-hint-color,#ebebeb)}.swagger-ui .model .property{color:var(--model-property-color,#999)}.swagger-ui .model .property.primitive{color:var(--model-property-primitive-color,#6b6b6b)}.swagger-ui table.model tr.property-row .star{color:var(--model-property-required-symbol-color,red)}.swagger-ui .model .external-docs,.swagger-ui table.model tr.description{color:var(--model-property-description,#666)}.swagger-ui table.model tr.extension{color:var(--model-property-extension,#777)}.swagger-ui section.models{border:1px solid var(--model-section-border-color,rgba(59,65,81,.3));border-radius:.5rem;padding:0;margin:0}.swagger-ui section.models.is-open h4{border-bottom:1px solid var(--model-section-border-color,rgba(59,65,81,.3))}.swagger-ui section.models h4{color:var(--model-section-header-color,#606060);padding:0}.swagger-ui section.models h4:hover{background:var(--model-section-header-hover-background-color,rgba(0,0,0,.02))}.swagger-ui section.models h4 svg{fill:var(--expand-model-svg-arrow-color,#707070)}.swagger-ui section.models h4 button.models-control{color:var(--model-section-header-color,#606060);padding:20px}.swagger-ui section.models h5{color:var(--model-section-little-header-color,#707070)}.swagger-ui section.models .model-container{background:var(--model-container-background-color,rgba(0,0,0,.05))}.swagger-ui section.models .model-container:hover{background:var(--model-container-hover-background-color,rgba(0,0,0,.07))}.swagger-ui .model-box{background:var(--model-box-background-color,rgba(0,0,0,.1))}.swagger-ui .model-title{color:var(--model-title-color,#505050)}.swagger-ui .model-deprecated-warning{color:var(--model-deprecated-warning-color,#f93e3e)}.swagger-ui .prop-type{color:var(--model-prop-type-color,#55a)}.swagger-ui .prop-format{color:var(--model-prop-format-color,#606060)}.swagger-ui .scopes h2{color:var(--swagger-main-color,#3b4151)}.swagger-ui .scopes h2 a{color:var(--swagger-info-link,#4990e2)}.swagger-ui .markdown pre,.swagger-ui .renderedMarkdown pre{color:var(--rendered-markdown-pre-color,#000)}.swagger-ui .markdown code,.swagger-ui .renderedMarkdown code{background:var(--rendered-markdown-code-background-color,rgba(0,0,0,.05));color:var(--rendered-markdown-code-color,#9012fe)}.scroll-to-top-wrapper{position:fixed;bottom:0;left:0;right:0;text-align:end;box-sizing:border-box;margin:0 auto;max-width:1460px;padding:0 20px;width:100%;pointer-events:none;z-index:1000}#scroll-to-top-btn{background-color:var(--scroll-to-top-button-background-color,#000000d1);border:1px solid var(--scroll-to-top-button-border-color,#d1c9c9);border-radius:50%;color:#fff;cursor:pointer;width:2.5rem;aspect-ratio:1;backdrop-filter:blur(20px);margin:10px;position:relative;transition:background-color .5s,opacity .6s .2s,translate .8s,scale .2s;opacity:0;translate:0 100px;box-shadow:0 0 10px 0 var(--scroll-to-top-button-shadow-color,rgba(0,0,0,.4))}#scroll-to-top-btn.showBtn{opacity:1;translate:0 0;pointer-events:auto}#scroll-to-top-btn::before{position:absolute;content:'';top:10px;left:12px;right:12px;background-color:var(--scroll-to-top-button-icon-color,#fff);border-radius:1rem;height:2px}#scroll-to-top-btn::after{position:absolute;content:'';top:15px;left:50%;border:solid var(--scroll-to-top-button-icon-color,#fff);border-width:0 2px 2px 0;padding:3px;transform:rotate(-135deg);translate:-50% 4px}#scroll-to-top-btn:focus-within,#scroll-to-top-btn:hover{background-color:var(--scroll-to-top-button-hover-background-color,#0d0d0dba)}#scroll-to-top-btn:focus-within::after,#scroll-to-top-btn:hover::after{animation:bounceUp 1.2s ease-in infinite}#scroll-to-top-btn:active{scale:0.95}@keyframes bounceUp{100%{translate:-50% 4px}25%{translate:-50% 6px}60%{translate:-50% -2px}}@-moz-keyframes bounceUp{100%{translate:-50% 4px}25%{translate:-50% 6px}60%{translate:-50% -2px}}@-webkit-keyframes bounceUp{100%{translate:-50% 4px}25%{translate:-50% 6px}60%{translate:-50% -2px}}@media only screen and (max-width:680px){.swagger-ui .opblock .opblock-summary-description,.swagger-ui .opblock .opblock-summary-operation-id,.swagger-ui .opblock .opblock-summary-path,.swagger-ui .opblock .opblock-summary-path__deprecated{word-break:break-all}}
\ No newline at end of file
diff --git a/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Styles/dark.css b/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Styles/dark.css
index ce542f8..4addccc 100644
--- a/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Styles/dark.css
+++ b/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Styles/dark.css
@@ -103,7 +103,7 @@
--download-contents-color: white;
--copy-to-clipboard-background-color: #7d8293;
--copy-to-clipboard-icon: url('data:image/svg+xml;charset=utf-8,') 50% no-repeat;
- --filter-operation-filter-input-border-color: #d8dde7;
+ --filter-operation-filter-input-border-color: #909090;
--filter-download-failed-color: red;
--filter-download-loading-color: #aaa;
--table-headers-example-color: #999;
diff --git a/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Styles/dark.min.css b/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Styles/dark.min.css
index 9d48b52..d8fa929 100644
--- a/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Styles/dark.min.css
+++ b/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/Styles/dark.min.css
@@ -1 +1 @@
-ο»Ώ/*Dark Theme https://github.com/teociaps/SwaggerUI.Themes */@media (prefers-color-scheme:dark){:root{--scrollbar-thumb-color:hsl(0, 0%, 45%);--scrollbar-thumb-hover-color:hsl(0, 0%, 55%);--body-background-color:#101010;--swagger-main-color:#ecf0f1;--loading-container-border-color:rgba(200, 200, 200, .1);--loading-container-border-top-color:rgba(255, 255, 255, .6);--errors-wrapper-background-color:rgb(255, 0, 0, .1);--errors-wrapper-border-color:#f93e3e;--errors-wrapper-errors-color:#606060;--topbar-background-color:black;--topbar-pinned-background-color:rgba(0, 0, 0, .3);--topbar-pinned-shadow-color:rgba(0, 0, 0, .6);--topbar-pin-icon-color:white;--topbar-border-color:gray;--topbar-select-border-color:#5b5b5b;--topbar-select-label-color:#dfd9d9;--topbar-download-url-button-background-color:#5b5b5b;--topbar-download-url-button-color:#dfd9d9;--swagger-info-link:#4990e2;--swagger-info-link-hover:#1b78e5;--api-version-background-color:#616161;--api-version-stamp-background-color:#23891f;--api-version-color:#cdcdcd;--auth-wrapper-background-color:black;--auth-wrapper-border-color:#383636;--auth-wrapper-box-shadow-color:#c1c1c1;--auth-container-border-bottom-color:#ebebeb;--auth-container-background-color:#ddd;--auth-container-errors-color:red;--btn-authorize-background-color:transparent;--btn-authorize-border-color:#49cc90;--btn-authorize-font-color:#49cc90;--btn-authorize-svg-fill-color:#49cc90;--opblock-tag-section-border-color:#312c2c;--opblock-border-color:black;--opblock-tag-section-expandcollapse-background-color:rgb(0 0 0 / 45%);--opblock-tag-background-color-hover:rgba(0, 0, 0, .1);--opblock-tag-pinned-background-color-hover:#0e0e0e;--opblock-tag-border-bottom-color:rgba(109, 113, 121, .3);--opblock-shadow-color:rgba(59, 59, 59, .19);--opblock-tabheader-underline-color:rgba(255, 255, 255, .6);--opblock-summary-svg-icons-color:white;--opblock-summary-border-bottom-color:black;--opblock-section-header-background-color:#0000004a;--opblock-section-header-shadow-color:rgba(0, 0, 0, .1);--opblock-summary-method-background-color:black;--opblock-summary-method-color:white;--opblock-summary-method-shadow-color:rgba(0, 0, 0, .1);--opblock-post-background-color:rgba(56, 203, 82, .1);--opblock-post-method-color:#49cc90;--opblock-post-border-color:#49cc90;--opblock-put-background-color:rgba(255, 189, 63, .1);--opblock-put-method-color:#eb9532;--opblock-put-border-color:#eb9532;--opblock-delete-background-color:rgba(255, 4, 4, .1);--opblock-delete-method-color:#c0392b;--opblock-delete-border-color:#c0392b;--opblock-get-background-color:rgb(33, 161, 255, .1);--opblock-get-method-color:#61affe;--opblock-get-border-color:#61affe;--opblock-patch-background-color:rgba(255, 0, 165, .1);--opblock-patch-method-color:#9b59b6;--opblock-patch-border-color:#9b59b6;--opblock-head-background-color:rgba(62, 73, 114, .1);--opblock-head-method-color:#3e4972;--opblock-head-border-color:#3e4972;--opblock-options-background-color:rgba(37, 64, 211, .1);--opblock-options-method-color:#263795;--opblock-options-border-color:#263795;--opblock-deprecated-background-color:hsla(0, 0%, 87%, .1);--opblock-deprecated-method-color:#a59595;--opblock-deprecated-border-color:#a59595;--swagger-tabs-divider-color:gray;--response-undocumented-color:#999;--response-control-media-type-color:green;--opblock-pre-microlight-background-color:#333;--opblock-pre-microlight-color:#ebe3e3;--download-contents-background-color:#7d8293;--download-contents-color:white;--copy-to-clipboard-background-color:#7d8293;--copy-to-clipboard-icon:url('data:image/svg+xml;charset=utf-8,') 50% no-repeat;--filter-operation-filter-input-border-color:#d8dde7;--filter-download-failed-color:red;--filter-download-loading-color:#aaa;--table-headers-example-color:#999;--table-thead-border-bottom-color:rgba(59, 65, 81, .2);--parameter-name-required-symbol-color:red;--parameter-name-required-color:rgba(255, 0, 0, .8);--parameter-in-extension-color:#878585;--parameter-deprecated-color:red;--button-background-color:transparent;--button-border-color:#a1a1a1;--button-shadow-color:rgba(0, 0, 0, .1);--button-shadow-hover-color:rgba(0, 0, 0, .3);--button-cancel-background-color:transparent;--button-cancel-border-color:#ff6060;--button-cancel-color:#ff6060;--button-execute-background-color:#4990e2;--button-execute-border-color:#5fa9ff;--button-execute-color:white;--button-invalid-background-color:#feebeb;--button-invalid-border-color:#f93e3e;--expand-operation-svg-arrow-color:#aaa;--expand-operation-svg-arrow-hover-color:white;--select-background-icon:#0c0c0c url('data:image/svg+xml;charset=utf-8,') right 10px center no-repeat;--select-border-color:#41444e;--select-shadow-color:rgba(0, 0, 0, .25);--input-background-color:#0c0c0c;--input-border-color:#6d6d6d;--input-invalid-background-color:#210101;--input-invalid-border-color:#f93e3e;--input-invalid-color:#f3ebeb;--input-disabled-background-color:#333333;--input-disabled-color:#757575;--select-disabled-border-color:#888;--textarea-disabled-background-color:#2b2b2b;--textarea-disabled-color:#b3b3b3;--textarea-background-color:hsla(0, 0%, 0%, .8);--textarea-border-focus-color:#a5a5a5;--textarea-curl-background-color:#41444e;--textarea-curl-color:#fff;--checkbox-color:#303030;--checkbox-label-item-background-color:#e8e8e8;--checkbox-label-item-shadow-color:#e8e8e8;--checkbox-label-item-icon:#e8e8e8 url('data:image/svg+xml;charset=utf-8,') 50% no-repeat;--dialog-backdrop-color:rgba(0, 0, 0, .8);--dialog-background-color:#161616;--dialog-border-color:#333436;--dialog-shadow-color:rgba(0, 0, 0, .2);--dialog-close-button-icon-color:#fff;--model-deprecated-color:#a0a0a0;--model-arrow-icon:url('data:image/svg+xml;charset=utf-8,') 50% no-repeat;--model-hint-background-color:rgba(0, 0, 0, .7);--model-hint-color:#ebebeb;--model-property-color:#999;--model-property-primitive-color:#676767;--model-property-required-symbol-color:red;--model-property-description:#b9b9b9;--model-property-extension:#8d8a8a;--model-section-border-color:rgba(96, 96, 96, .3);--model-section-header-color:white;--model-section-header-hover-background-color:rgba(0, 0, 0, .02);--expand-model-svg-arrow-color:white;--model-section-little-header-color:#707070;--model-container-background-color:rgba(20, 20, 20, .1);--model-container-hover-background-color:rgba(0, 0, 0, .1);--model-box-background-color:rgba(0, 0, 0, .15);--model-title-color:#bec6cf;--model-deprecated-warning-color:#f93e3e;--model-prop-type-color:#6464db;--model-prop-format-color:#b7b7b7;--rendered-markdown-pre-color:#000;--rendered-markdown-code-background-color:rgba(0, 0, 0, .05);--rendered-markdown-code-color:#9012fe;--scroll-to-top-button-background-color:#00000085;--scroll-to-top-button-hover-background-color:#0d0d0d85;--scroll-to-top-button-border-color:#232323;--scroll-to-top-button-icon-color:#d1cccc;--scroll-to-top-button-shadow-color:rgba(0, 0, 0, .3)}}
\ No newline at end of file
+ο»Ώ/*Dark Theme https://github.com/teociaps/SwaggerUI.Themes */@media (prefers-color-scheme:dark){:root{--scrollbar-thumb-color:hsl(0, 0%, 45%);--scrollbar-thumb-hover-color:hsl(0, 0%, 55%);--body-background-color:#101010;--swagger-main-color:#ecf0f1;--loading-container-border-color:rgba(200, 200, 200, .1);--loading-container-border-top-color:rgba(255, 255, 255, .6);--errors-wrapper-background-color:rgb(255, 0, 0, .1);--errors-wrapper-border-color:#f93e3e;--errors-wrapper-errors-color:#606060;--topbar-background-color:black;--topbar-pinned-background-color:rgba(0, 0, 0, .3);--topbar-pinned-shadow-color:rgba(0, 0, 0, .6);--topbar-pin-icon-color:white;--topbar-border-color:gray;--topbar-select-border-color:#5b5b5b;--topbar-select-label-color:#dfd9d9;--topbar-download-url-button-background-color:#5b5b5b;--topbar-download-url-button-color:#dfd9d9;--swagger-info-link:#4990e2;--swagger-info-link-hover:#1b78e5;--api-version-background-color:#616161;--api-version-stamp-background-color:#23891f;--api-version-color:#cdcdcd;--auth-wrapper-background-color:black;--auth-wrapper-border-color:#383636;--auth-wrapper-box-shadow-color:#c1c1c1;--auth-container-border-bottom-color:#ebebeb;--auth-container-background-color:#ddd;--auth-container-errors-color:red;--btn-authorize-background-color:transparent;--btn-authorize-border-color:#49cc90;--btn-authorize-font-color:#49cc90;--btn-authorize-svg-fill-color:#49cc90;--opblock-tag-section-border-color:#312c2c;--opblock-border-color:black;--opblock-tag-section-expandcollapse-background-color:rgb(0 0 0 / 45%);--opblock-tag-background-color-hover:rgba(0, 0, 0, .1);--opblock-tag-pinned-background-color-hover:#0e0e0e;--opblock-tag-border-bottom-color:rgba(109, 113, 121, .3);--opblock-shadow-color:rgba(59, 59, 59, .19);--opblock-tabheader-underline-color:rgba(255, 255, 255, .6);--opblock-summary-svg-icons-color:white;--opblock-summary-border-bottom-color:black;--opblock-section-header-background-color:#0000004a;--opblock-section-header-shadow-color:rgba(0, 0, 0, .1);--opblock-summary-method-background-color:black;--opblock-summary-method-color:white;--opblock-summary-method-shadow-color:rgba(0, 0, 0, .1);--opblock-post-background-color:rgba(56, 203, 82, .1);--opblock-post-method-color:#49cc90;--opblock-post-border-color:#49cc90;--opblock-put-background-color:rgba(255, 189, 63, .1);--opblock-put-method-color:#eb9532;--opblock-put-border-color:#eb9532;--opblock-delete-background-color:rgba(255, 4, 4, .1);--opblock-delete-method-color:#c0392b;--opblock-delete-border-color:#c0392b;--opblock-get-background-color:rgb(33, 161, 255, .1);--opblock-get-method-color:#61affe;--opblock-get-border-color:#61affe;--opblock-patch-background-color:rgba(255, 0, 165, .1);--opblock-patch-method-color:#9b59b6;--opblock-patch-border-color:#9b59b6;--opblock-head-background-color:rgba(62, 73, 114, .1);--opblock-head-method-color:#3e4972;--opblock-head-border-color:#3e4972;--opblock-options-background-color:rgba(37, 64, 211, .1);--opblock-options-method-color:#263795;--opblock-options-border-color:#263795;--opblock-deprecated-background-color:hsla(0, 0%, 87%, .1);--opblock-deprecated-method-color:#a59595;--opblock-deprecated-border-color:#a59595;--swagger-tabs-divider-color:gray;--response-undocumented-color:#999;--response-control-media-type-color:green;--opblock-pre-microlight-background-color:#333;--opblock-pre-microlight-color:#ebe3e3;--download-contents-background-color:#7d8293;--download-contents-color:white;--copy-to-clipboard-background-color:#7d8293;--copy-to-clipboard-icon:url('data:image/svg+xml;charset=utf-8,') 50% no-repeat;--filter-operation-filter-input-border-color:#909090;--filter-download-failed-color:red;--filter-download-loading-color:#aaa;--table-headers-example-color:#999;--table-thead-border-bottom-color:rgba(59, 65, 81, .2);--parameter-name-required-symbol-color:red;--parameter-name-required-color:rgba(255, 0, 0, .8);--parameter-in-extension-color:#878585;--parameter-deprecated-color:red;--button-background-color:transparent;--button-border-color:#a1a1a1;--button-shadow-color:rgba(0, 0, 0, .1);--button-shadow-hover-color:rgba(0, 0, 0, .3);--button-cancel-background-color:transparent;--button-cancel-border-color:#ff6060;--button-cancel-color:#ff6060;--button-execute-background-color:#4990e2;--button-execute-border-color:#5fa9ff;--button-execute-color:white;--button-invalid-background-color:#feebeb;--button-invalid-border-color:#f93e3e;--expand-operation-svg-arrow-color:#aaa;--expand-operation-svg-arrow-hover-color:white;--select-background-icon:#0c0c0c url('data:image/svg+xml;charset=utf-8,') right 10px center no-repeat;--select-border-color:#41444e;--select-shadow-color:rgba(0, 0, 0, .25);--input-background-color:#0c0c0c;--input-border-color:#6d6d6d;--input-invalid-background-color:#210101;--input-invalid-border-color:#f93e3e;--input-invalid-color:#f3ebeb;--input-disabled-background-color:#333333;--input-disabled-color:#757575;--select-disabled-border-color:#888;--textarea-disabled-background-color:#2b2b2b;--textarea-disabled-color:#b3b3b3;--textarea-background-color:hsla(0, 0%, 0%, .8);--textarea-border-focus-color:#a5a5a5;--textarea-curl-background-color:#41444e;--textarea-curl-color:#fff;--checkbox-color:#303030;--checkbox-label-item-background-color:#e8e8e8;--checkbox-label-item-shadow-color:#e8e8e8;--checkbox-label-item-icon:#e8e8e8 url('data:image/svg+xml;charset=utf-8,') 50% no-repeat;--dialog-backdrop-color:rgba(0, 0, 0, .8);--dialog-background-color:#161616;--dialog-border-color:#333436;--dialog-shadow-color:rgba(0, 0, 0, .2);--dialog-close-button-icon-color:#fff;--model-deprecated-color:#a0a0a0;--model-arrow-icon:url('data:image/svg+xml;charset=utf-8,') 50% no-repeat;--model-hint-background-color:rgba(0, 0, 0, .7);--model-hint-color:#ebebeb;--model-property-color:#999;--model-property-primitive-color:#676767;--model-property-required-symbol-color:red;--model-property-description:#b9b9b9;--model-property-extension:#8d8a8a;--model-section-border-color:rgba(96, 96, 96, .3);--model-section-header-color:white;--model-section-header-hover-background-color:rgba(0, 0, 0, .02);--expand-model-svg-arrow-color:white;--model-section-little-header-color:#707070;--model-container-background-color:rgba(20, 20, 20, .1);--model-container-hover-background-color:rgba(0, 0, 0, .1);--model-box-background-color:rgba(0, 0, 0, .15);--model-title-color:#bec6cf;--model-deprecated-warning-color:#f93e3e;--model-prop-type-color:#6464db;--model-prop-format-color:#b7b7b7;--rendered-markdown-pre-color:#000;--rendered-markdown-code-background-color:rgba(0, 0, 0, .05);--rendered-markdown-code-color:#9012fe;--scroll-to-top-button-background-color:#00000085;--scroll-to-top-button-hover-background-color:#0d0d0d85;--scroll-to-top-button-border-color:#232323;--scroll-to-top-button-icon-color:#d1cccc;--scroll-to-top-button-shadow-color:rgba(0, 0, 0, .3)}}
\ No newline at end of file
diff --git a/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/ThemeSwitcher.cs b/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/ThemeSwitcher.cs
index 8bfa66c..cd5528c 100644
--- a/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/ThemeSwitcher.cs
+++ b/src/AspNetCore.Swagger.Themes.Common/AspNetCore/Swagger/Themes/ThemeSwitcher.cs
@@ -1,4 +1,5 @@
ο»Ώusing Microsoft.AspNetCore.Builder;
+using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Reflection;
@@ -15,9 +16,9 @@ internal static class ThemeSwitcher
Theme.Dark, Theme.Light, Theme.Forest, Theme.DeepSea, Theme.Desert, Theme.Futuristic
}.ToFrozenSet();
- private static readonly Dictionary s_registeredThemes = new(StringComparer.OrdinalIgnoreCase);
+ private static readonly ConcurrentDictionary s_registeredThemes = new(StringComparer.OrdinalIgnoreCase);
private static FrozenDictionary s_frozenThemes;
- private static readonly HashSet s_scannedTypes = [];
+ private static readonly ConcurrentDictionary s_scannedTypes = new();
///
/// Configures theme switcher for a given theme and options.
@@ -64,14 +65,11 @@ internal static void RegisterTheme(BaseTheme theme, string cssPath, bool isStand
{
var themeName = theme.GetThemeName();
- if (s_registeredThemes.ContainsKey(themeName))
- return;
-
- s_registeredThemes[themeName] = new RegisteredTheme(
+ s_registeredThemes.TryAdd(themeName, new RegisteredTheme(
theme,
FileProvider.StylesPath + cssPath,
isStandalone
- );
+ ));
}
///
@@ -82,14 +80,11 @@ internal static void RegisterCustomTheme(string cssFilename, bool isStandalone)
var themeName = ExtractThemeName(cssFilename);
var cssPath = FileProvider.StylesPath + cssFilename;
- if (s_registeredThemes.ContainsKey(themeName))
- return;
-
- s_registeredThemes[themeName] = new RegisteredTheme(
+ s_registeredThemes.TryAdd(themeName, new RegisteredTheme(
null,
cssPath,
isStandalone
- );
+ ));
}
///
@@ -188,7 +183,7 @@ private static void ScanAssemblyForThemes(Assembly assembly)
///
private static void ScanTypeForThemes(Type type)
{
- if (!s_scannedTypes.Add(type))
+ if (!s_scannedTypes.TryAdd(type, 0))
return;
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Static)
diff --git a/src/AspNetCore.SwaggerUI.Extensions/AspNetCore.SwaggerUI.Extensions.csproj b/src/AspNetCore.SwaggerUI.Extensions/AspNetCore.SwaggerUI.Extensions.csproj
new file mode 100644
index 0000000..393052a
--- /dev/null
+++ b/src/AspNetCore.SwaggerUI.Extensions/AspNetCore.SwaggerUI.Extensions.csproj
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+ AspNetCore.SwaggerUI.Extensions
+
+ Enhance your ASP.NET Core Swagger UI with functional extensions like API Counter.
+
+
+ swagger;swagger-ui;swaggerui;swashbuckle;aspnetcore;webapi;openapi;extension;api-counter;operation-count;tag-extensions
+
+ true
+
+
+
+
+
+
+
+
+
diff --git a/src/AspNetCore.SwaggerUI.Extensions/AspNetCore/Swagger/Extensions/Filters/AppendOperationCountToTagDescriptionFilter.cs b/src/AspNetCore.SwaggerUI.Extensions/AspNetCore/Swagger/Extensions/Filters/AppendOperationCountToTagDescriptionFilter.cs
new file mode 100644
index 0000000..ce60eb9
--- /dev/null
+++ b/src/AspNetCore.SwaggerUI.Extensions/AspNetCore/Swagger/Extensions/Filters/AppendOperationCountToTagDescriptionFilter.cs
@@ -0,0 +1,100 @@
+#if NET10_0_OR_GREATER
+using Microsoft.OpenApi;
+#else
+using Microsoft.OpenApi.Models;
+#endif
+using Swashbuckle.AspNetCore.SwaggerGen;
+
+namespace AspNetCore.Swagger.Extensions.Filters;
+
+///
+/// Document filter that appends operation count to tag descriptions in Swagger UI.
+///
+///
+/// This filter modifies the OpenAPI document at generation time to include
+/// the number of operations (endpoints) in each tag's description.
+///
+public sealed class AppendOperationCountToTagDescriptionFilter : IDocumentFilter
+{
+ private readonly string _messageTemplate;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The message template to use. Must contain {0} placeholder for the count.
+ /// Default: " (operations: {0})"
+ ///
+ ///
+ /// Thrown when does not contain {0} placeholder.
+ ///
+ public AppendOperationCountToTagDescriptionFilter(string messageTemplate = " (operations: {0})")
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(messageTemplate);
+
+ if (!messageTemplate.Contains("{0}"))
+ {
+ throw new ArgumentException("The message template must contain '{0}' placeholder.", nameof(messageTemplate));
+ }
+
+ _messageTemplate = messageTemplate;
+ }
+
+ ///
+ /// Applies the filter to the OpenAPI document.
+ ///
+ /// The OpenAPI document to modify.
+ /// The document filter context.
+ public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
+ {
+ // Count operations per tag
+ var tagOperationCount = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var path in swaggerDoc.Paths.Values)
+ {
+ foreach (var operation in path.Operations.Values)
+ {
+ if (operation?.Tags is null || operation.Tags.Count == 0)
+ continue;
+
+ foreach (var tag in operation.Tags.Select(t => t.Name))
+ {
+ if (tagOperationCount.TryGetValue(tag, out int value))
+ tagOperationCount[tag] = ++value;
+ else
+ tagOperationCount[tag] = 1;
+ }
+ }
+ }
+
+ // If no counts found, nothing to do
+ if (tagOperationCount.Count == 0)
+ return;
+
+ // Ensure Tags collection exists
+#if NET10_0_OR_GREATER
+ swaggerDoc.Tags ??= new HashSet();
+#else
+ swaggerDoc.Tags ??= [];
+#endif
+
+ // Create tags if they don't exist
+ foreach (var tagName in tagOperationCount.Keys)
+ {
+ if (!swaggerDoc.Tags.Any(t => t.Name.Equals(tagName, StringComparison.OrdinalIgnoreCase)))
+ {
+ swaggerDoc.Tags.Add(new OpenApiTag { Name = tagName });
+ }
+ }
+
+ // Append count to tag descriptions
+ foreach (var tag in swaggerDoc.Tags)
+ {
+ if (tagOperationCount.TryGetValue(tag.Name, out var count))
+ {
+ tag.Description ??= string.Empty;
+ tag.Description += string.Format(_messageTemplate, count);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AspNetCore.SwaggerUI.Extensions/Microsoft/Extensions/DependencyInjection/SwaggerGenOptionsExtensions.cs b/src/AspNetCore.SwaggerUI.Extensions/Microsoft/Extensions/DependencyInjection/SwaggerGenOptionsExtensions.cs
new file mode 100644
index 0000000..26b41d5
--- /dev/null
+++ b/src/AspNetCore.SwaggerUI.Extensions/Microsoft/Extensions/DependencyInjection/SwaggerGenOptionsExtensions.cs
@@ -0,0 +1,35 @@
+using AspNetCore.Swagger.Extensions.Filters;
+using Swashbuckle.AspNetCore.SwaggerGen;
+
+namespace Microsoft.Extensions.DependencyInjection;
+
+///
+/// Extension methods for configuring Swagger generation with operation count feature.
+///
+public static class SwaggerGenOptionsExtensions
+{
+ ///
+ /// Appends operation count to tag descriptions in Swagger UI.
+ ///
+ /// The Swagger generation options.
+ ///
+ /// Optional message template. Must contain {0} placeholder for the count.
+ /// Default: " (operations: {0})"
+ ///
+ ///
+ ///
+ /// services.AddSwaggerGen(c =>
+ /// {
+ /// c.AppendOperationCountToTags();
+ /// // Or with custom template:
+ /// c.AppendOperationCountToTags(" [{0} endpoints]");
+ /// });
+ ///
+ ///
+ public static void AppendOperationCountToTags(
+ this SwaggerGenOptions options,
+ string messageTemplate = " (operations: {0})")
+ {
+ options.DocumentFilter(messageTemplate);
+ }
+}
\ No newline at end of file
diff --git a/src/AspNetCore.SwaggerUI.Extensions/package-readme.md b/src/AspNetCore.SwaggerUI.Extensions/package-readme.md
new file mode 100644
index 0000000..cd78785
--- /dev/null
+++ b/src/AspNetCore.SwaggerUI.Extensions/package-readme.md
@@ -0,0 +1,48 @@
+ο»Ώ# AspNetCore.SwaggerUI.Extensions
+
+### Enhance your ASP.NET Core Swagger UI with functional extensions that go beyond theming!
+
+**Automatic operation counts** β’ **Zero-config setup** β’ _**...and more coming soon!**_
+
+**[Get Started](https://github.com/teociaps/SwaggerUI.Themes/wiki/Getting-Started)** β’ **[Full Documentation](https://github.com/teociaps/SwaggerUI.Themes/wiki)**
+
+## π Quick Start
+
+```bash
+dotnet add package AspNetCore.SwaggerUI.Extensions
+```
+
+```csharp
+builder.Services.AddSwaggerGen(c =>
+{
+ c.AppendOperationCountToTags();
+});
+```
+
+> **Note**: `AddSwaggerGen()` is provided by Swashbuckle.AspNetCore. This package adds convenient extension methods on top of it.
+
+## β¨ Features
+
+- **API Counter** - Automatically display operation counts in tag descriptions
+- _...more extensions coming soon!_
+
+## π Basic Usage Examples
+
+```csharp
+builder.Services.AddSwaggerGen(c =>
+{
+ // Append operation count to tag descriptions
+ c.AppendOperationCountToTags();
+
+ // Or with a custom template
+ c.AppendOperationCountToTags(" [{0} endpoints]");
+});
+```
+
+## Related Packages
+
+- **[AspNetCore.SwaggerUI.Themes](https://www.nuget.org/packages/AspNetCore.SwaggerUI.Themes/)** - Runtime theme switching and customization for Swashbuckle
+
+---
+
+#### Discover all the features and customization options in the [documentation](https://github.com/teociaps/SwaggerUI.Themes/wiki)!
\ No newline at end of file
diff --git a/src/AspNetCore.SwaggerUI.Extensions/readme.txt b/src/AspNetCore.SwaggerUI.Extensions/readme.txt
new file mode 100644
index 0000000..c7c9bf2
--- /dev/null
+++ b/src/AspNetCore.SwaggerUI.Extensions/readme.txt
@@ -0,0 +1,24 @@
+ο»Ώ+===================================================================================+
+| AspNetCore.SwaggerUI.Extensions β’ RELEASE NOTES - v1.0 |
++===================================================================================+
+
+NEW FEATURES
+-------------------------------------------------------------------------------------
+- API Counter: Automatically append operation counts to tag descriptions
+ Enable it with: c.AppendOperationCountToTags();
+
+
+QUICK START
+-------------------------------------------------------------------------------------
+API Counter:
+ builder.Services.AddSwaggerGen(c => c.AppendOperationCountToTags());
+
+
+DOCUMENTATION
+-------------------------------------------------------------------------------------
+
+Full docs: https://github.com/teociaps/SwaggerUI.Themes/wiki
+Troubleshooting: https://github.com/teociaps/SwaggerUI.Themes/wiki/Troubleshooting
+Repository: https://github.com/teociaps/SwaggerUI.Themes
+
+=====================================================================================
\ No newline at end of file
diff --git a/src/AspNetCore.SwaggerUI.Themes/AspNetCore.SwaggerUI.Themes.csproj b/src/AspNetCore.SwaggerUI.Themes/AspNetCore.SwaggerUI.Themes.csproj
index c297dd7..abe46e9 100644
--- a/src/AspNetCore.SwaggerUI.Themes/AspNetCore.SwaggerUI.Themes.csproj
+++ b/src/AspNetCore.SwaggerUI.Themes/AspNetCore.SwaggerUI.Themes.csproj
@@ -1,17 +1,9 @@
ο»Ώ
+
-
-
- 3
- 0
- 2
-
- $(MajorVersion).$(MinorVersion).$(PatchVersion)$(VersionSuffix)
-
-
AspNetCore.SwaggerUI.Themes
diff --git a/src/AspNetCore.SwaggerUI.Themes/Microsoft/AspNetCore/Builder/SwaggerUIOptionsExtensions.cs b/src/AspNetCore.SwaggerUI.Themes/Microsoft/AspNetCore/Builder/SwaggerUIOptionsExtensions.cs
index 92a0cf3..891b419 100644
--- a/src/AspNetCore.SwaggerUI.Themes/Microsoft/AspNetCore/Builder/SwaggerUIOptionsExtensions.cs
+++ b/src/AspNetCore.SwaggerUI.Themes/Microsoft/AspNetCore/Builder/SwaggerUIOptionsExtensions.cs
@@ -57,6 +57,20 @@ public void EnableAllAdvancedOptions(ThemeSwitcherOptions switcherOptions = null
public void EnablePinnableTopbar() =>
options.ConfigObject.AdditionalItems.EnablePinnableTopbar();
+ ///
+ /// Enables the operation filter box, optionally making it pinnable.
+ ///
+ ///
+ /// If , the filter box can be pinned in place.
+ ///
+ public void EnableFilter(bool pinnable)
+ {
+ options.EnableFilter();
+
+ if (pinnable)
+ options.ConfigObject.AdditionalItems.EnablePinnableFilterBar();
+ }
+
///
/// Shows a button to scroll back to the top of the page.
///
diff --git a/src/AspNetCore.SwaggerUI.Themes/package-readme.md b/src/AspNetCore.SwaggerUI.Themes/package-readme.md
index 6d78629..7c7bc7f 100644
--- a/src/AspNetCore.SwaggerUI.Themes/package-readme.md
+++ b/src/AspNetCore.SwaggerUI.Themes/package-readme.md
@@ -21,6 +21,9 @@ app.UseSwaggerUI(Theme.Dark);
// Or enable runtime theme switcher!
app.UseSwaggerUI(Theme.Dark, c => c.EnableThemeSwitcher());
+
+// Or enable a pinnable filter box
+app.UseSwaggerUI(Theme.Dark, c => c.EnableFilter(pinnable: true));
```
> **Note**: The `UseSwaggerUI()` method is provided by Swashbuckle.AspNetCore. This package adds convenient overloads to apply themes seamlessly.
diff --git a/src/AspNetCore.SwaggerUI.Themes/readme.txt b/src/AspNetCore.SwaggerUI.Themes/readme.txt
index a406be4..e6da52a 100644
--- a/src/AspNetCore.SwaggerUI.Themes/readme.txt
+++ b/src/AspNetCore.SwaggerUI.Themes/readme.txt
@@ -1,57 +1,31 @@
ο»Ώ+===================================================================================+
-| AspNetCore.SwaggerUI.Themes β’ RELEASE NOTES - v3 |
+| AspNetCore.SwaggerUI.Themes β’ RELEASE NOTES - v3.1 |
+===================================================================================+
-BREAKING CHANGES
--------------------------------------------------------------------------------------
-- API redesign: Style -> Theme (all classes renamed)
-- Classic themes removed (modern themes only)
-- .NET 6 & 7 support discontinued
-
-See migration guide: https://github.com/teociaps/SwaggerUI.Themes/wiki/Migration-v3
-
-
NEW FEATURES
-------------------------------------------------------------------------------------
-- π₯ Runtime Theme Switcher: Change themes on-the-fly without refreshing the page
-- Auto-Discovery: Custom themes automatically available in the theme switcher
-- Nested Folder Support: Organize your themes in subfolders
-- Standalone Themes: Create CSS-only themes with zero dependencies
-- Smart Filename Resolution: Support standard and standalone variants simultaneously
-- Minified Stylesheet Support: Optimize load times with minified CSS
-
-
-PLATFORM & DEPENDENCIES
--------------------------------------------------------------------------------------
-- .NET 10 support added
-- Swashbuckle.AspNetCore updated to v9.0.6
+- Pinnable Filter Bar: Pin the operation filter box in place, just like the topbar
+ Enable it with: c.EnableFilter(pinnable: true);
IMPROVEMENTS
-------------------------------------------------------------------------------------
-- Unified modern theme system with consistent defaults
-- Advanced features now available on all themes
-- Smaller package footprint (optimized assets)
-- Improved performance and clarity throughout
+- Pinnable topbar now remembers its pinned/unpinned state across page reloads
+- Improved theme asset serving performance
+- Fixed a thread-safety issue that could affect parallel test execution
QUICK START
-------------------------------------------------------------------------------------
-Basic theme:
- app.UseSwaggerUI(Theme.Dark);
-
-Enable theme switcher:
- app.UseSwaggerUI(Theme.Dark, c => c.EnableThemeSwitcher());
-
-All features:
- app.UseSwaggerUI(Theme.Dark, c => c.EnableAllAdvancedOptions());
+Pinnable filter bar:
+ app.UseSwaggerUI(Theme.Dark, c => c.EnableFilter(pinnable: true));
DOCUMENTATION
-------------------------------------------------------------------------------------
-Full docs: https://github.com/teociaps/SwaggerUI.Themes/wiki
-Migration: https://github.com/teociaps/SwaggerUI.Themes/wiki/Migration-v3
-Repository: https://github.com/teociaps/SwaggerUI.Themes
+Full docs: https://github.com/teociaps/SwaggerUI.Themes/wiki
+Troubleshooting: https://github.com/teociaps/SwaggerUI.Themes/wiki/Troubleshooting
+Repository: https://github.com/teociaps/SwaggerUI.Themes
=====================================================================================
\ No newline at end of file
diff --git a/src/NSwag.AspNetCore.Extensions/AspNetCore/Swagger/Extensions/Processors/AppendOperationCountToTagDescriptionProcessor.cs b/src/NSwag.AspNetCore.Extensions/AspNetCore/Swagger/Extensions/Processors/AppendOperationCountToTagDescriptionProcessor.cs
new file mode 100644
index 0000000..f26cc8e
--- /dev/null
+++ b/src/NSwag.AspNetCore.Extensions/AspNetCore/Swagger/Extensions/Processors/AppendOperationCountToTagDescriptionProcessor.cs
@@ -0,0 +1,94 @@
+using NSwag;
+using NSwag.Generation.Processors;
+using NSwag.Generation.Processors.Contexts;
+
+namespace AspNetCore.Swagger.Extensions.Processors;
+
+///
+/// Document processor that appends operation count to tag descriptions in Swagger UI.
+///
+///
+/// This processor modifies the OpenAPI document at generation time to include
+/// the number of operations (endpoints) in each tag's description.
+///
+public sealed class AppendOperationCountToTagDescriptionProcessor : IDocumentProcessor
+{
+ private readonly string _messageTemplate;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The message template to use. Must contain {0} placeholder for the count.
+ /// Default: " (operations: {0})"
+ ///
+ ///
+ /// Thrown when does not contain {0} placeholder.
+ ///
+ public AppendOperationCountToTagDescriptionProcessor(string messageTemplate = " (operations: {0})")
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(messageTemplate);
+
+ if (!messageTemplate.Contains("{0}"))
+ {
+ throw new ArgumentException("The message template must contain '{0}' placeholder.", nameof(messageTemplate));
+ }
+
+ _messageTemplate = messageTemplate;
+ }
+
+ ///
+ /// Processes the OpenAPI document.
+ ///
+ /// The document processor context.
+ public void Process(DocumentProcessorContext context)
+ {
+ var document = context.Document;
+
+ // Count operations per tag
+ var tagOperationCount = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var path in document.Paths.Values)
+ {
+ foreach (var operation in path.Values)
+ {
+ if (operation?.Tags is null || operation.Tags.Count == 0)
+ continue;
+
+ foreach (var tag in operation.Tags)
+ {
+ if (tagOperationCount.TryGetValue(tag, out int value))
+ tagOperationCount[tag] = ++value;
+ else
+ tagOperationCount[tag] = 1;
+ }
+ }
+ }
+
+ // If no counts found, nothing to do
+ if (tagOperationCount.Count == 0)
+ return;
+
+ // Ensure Tags collection exists
+ document.Tags ??= new List();
+
+ // Create tags if they don't exist
+ foreach (var tagName in tagOperationCount.Keys)
+ {
+ if (!document.Tags.Any(t => t.Name.Equals(tagName, StringComparison.OrdinalIgnoreCase)))
+ {
+ document.Tags.Add(new NSwag.OpenApiTag { Name = tagName });
+ }
+ }
+
+ // Append count to tag descriptions
+ foreach (var tag in document.Tags)
+ {
+ if (tagOperationCount.TryGetValue(tag.Name, out var count))
+ {
+ tag.Description ??= string.Empty;
+ tag.Description += string.Format(_messageTemplate, count);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/NSwag.AspNetCore.Extensions/Microsoft/Extensions/DependencyInjection/AspNetCoreOpenApiDocumentGeneratorSettingsExtensions.cs b/src/NSwag.AspNetCore.Extensions/Microsoft/Extensions/DependencyInjection/AspNetCoreOpenApiDocumentGeneratorSettingsExtensions.cs
new file mode 100644
index 0000000..b299410
--- /dev/null
+++ b/src/NSwag.AspNetCore.Extensions/Microsoft/Extensions/DependencyInjection/AspNetCoreOpenApiDocumentGeneratorSettingsExtensions.cs
@@ -0,0 +1,36 @@
+using AspNetCore.Swagger.Extensions.Processors;
+using NSwag.Generation.AspNetCore;
+
+namespace Microsoft.Extensions.DependencyInjection;
+
+///
+/// Extension methods for configuring OpenAPI document generation with operation count feature.
+///
+public static class AspNetCoreOpenApiDocumentGeneratorSettingsExtensions
+{
+ ///
+ /// Appends operation count to tag descriptions in Swagger UI.
+ ///
+ /// The OpenAPI document generator settings.
+ ///
+ /// Optional message template. Must contain {0} placeholder for the count.
+ /// Default: " (operations: {0})"
+ ///
+ ///
+ ///
+ /// services.AddOpenApiDocument(c =>
+ /// {
+ /// c.AppendOperationCountToTags();
+ /// // Or with custom template:
+ /// c.AppendOperationCountToTags(" [{0} endpoints]");
+ /// });
+ ///
+ ///
+ public static void AppendOperationCountToTags(
+ this AspNetCoreOpenApiDocumentGeneratorSettings settings,
+ string messageTemplate = " (operations: {0})")
+ {
+ settings.DocumentProcessors.Add(
+ new AppendOperationCountToTagDescriptionProcessor(messageTemplate));
+ }
+}
diff --git a/src/NSwag.AspNetCore.Extensions/NSwag.AspNetCore.Extensions.csproj b/src/NSwag.AspNetCore.Extensions/NSwag.AspNetCore.Extensions.csproj
new file mode 100644
index 0000000..2b36d1c
--- /dev/null
+++ b/src/NSwag.AspNetCore.Extensions/NSwag.AspNetCore.Extensions.csproj
@@ -0,0 +1,22 @@
+ο»Ώ
+
+
+
+
+
+
+ NSwag.AspNetCore.Extensions
+
+ Enhance your ASP.NET Core Swagger UI with functional extensions like API Counter.
+
+
+ swagger;swagger-ui;swaggerui;nswag;aspnetcore;webapi;openapi;extension;api-counter;operation-count;tag-extensions
+
+ true
+
+
+
+
+
+
+
diff --git a/src/NSwag.AspNetCore.Extensions/package-readme.md b/src/NSwag.AspNetCore.Extensions/package-readme.md
new file mode 100644
index 0000000..bc10a09
--- /dev/null
+++ b/src/NSwag.AspNetCore.Extensions/package-readme.md
@@ -0,0 +1,48 @@
+ο»Ώ# NSwag.AspNetCore.Extensions
+
+### Enhance your ASP.NET Core Swagger UI with functional extensions that go beyond theming!
+
+**Automatic operation counts** β’ **Zero-config setup** β’ _**...and more coming soon!**_
+
+**[Get Started](https://github.com/teociaps/SwaggerUI.Themes/wiki/Getting-Started)** β’ **[Full Documentation](https://github.com/teociaps/SwaggerUI.Themes/wiki)**
+
+## π Quick Start
+
+```bash
+dotnet add package NSwag.AspNetCore.Extensions
+```
+
+```csharp
+builder.Services.AddOpenApiDocument(c =>
+{
+ c.AppendOperationCountToTags();
+});
+```
+
+> **Note**: `AddOpenApiDocument()` is provided by NSwag.AspNetCore. This package adds convenient extension methods on top of it.
+
+## β¨ Features
+
+- **API Counter** - Automatically display operation counts in tag descriptions
+- _...more extensions coming soon!_
+
+## π Basic Usage Examples
+
+```csharp
+builder.Services.AddOpenApiDocument(c =>
+{
+ // Append operation count to tag descriptions
+ c.AppendOperationCountToTags();
+
+ // Or with a custom template
+ c.AppendOperationCountToTags(" [{0} endpoints]");
+});
+```
+
+## Related Packages
+
+- **[NSwag.AspNetCore.Themes](https://www.nuget.org/packages/NSwag.AspNetCore.Themes/)** - Runtime theme switching and customization for NSwag
+
+---
+
+#### Discover all the features and customization options in the [documentation](https://github.com/teociaps/SwaggerUI.Themes/wiki)!
\ No newline at end of file
diff --git a/src/NSwag.AspNetCore.Extensions/readme.txt b/src/NSwag.AspNetCore.Extensions/readme.txt
new file mode 100644
index 0000000..20470f7
--- /dev/null
+++ b/src/NSwag.AspNetCore.Extensions/readme.txt
@@ -0,0 +1,24 @@
+ο»Ώ+===================================================================================+
+| NSwag.AspNetCore.Extensions β’ RELEASE NOTES - v1.0 |
++===================================================================================+
+
+NEW FEATURES
+-------------------------------------------------------------------------------------
+- API Counter: Automatically append operation counts to tag descriptions
+ Enable it with: c.AppendOperationCountToTags();
+
+
+QUICK START
+-------------------------------------------------------------------------------------
+API Counter:
+ builder.Services.AddOpenApiDocument(c => c.AppendOperationCountToTags());
+
+
+DOCUMENTATION
+-------------------------------------------------------------------------------------
+
+Full docs: https://github.com/teociaps/SwaggerUI.Themes/wiki
+Troubleshooting: https://github.com/teociaps/SwaggerUI.Themes/wiki/Troubleshooting
+Repository: https://github.com/teociaps/SwaggerUI.Themes
+
+=====================================================================================
\ No newline at end of file
diff --git a/src/NSwag.AspNetCore.Themes/Microsoft/AspNetCore/Builder/SwaggerUiSettingsExtensions.cs b/src/NSwag.AspNetCore.Themes/Microsoft/AspNetCore/Builder/SwaggerUiSettingsExtensions.cs
index 066efbf..b868c5d 100644
--- a/src/NSwag.AspNetCore.Themes/Microsoft/AspNetCore/Builder/SwaggerUiSettingsExtensions.cs
+++ b/src/NSwag.AspNetCore.Themes/Microsoft/AspNetCore/Builder/SwaggerUiSettingsExtensions.cs
@@ -57,6 +57,31 @@ public void EnableAllAdvancedOptions(ThemeSwitcherOptions switcherOptions = null
public void EnablePinnableTopbar() =>
settings.AdditionalSettings.EnablePinnableTopbar();
+ ///
+ /// Enables the operation filter box.
+ ///
+ ///
+ /// Sets the raw filter key consumed directly by the swagger-ui bundle's own
+ /// SwaggerUIBundle({...}) initializer, as opposed to the other options here
+ /// which drive this library's own theme JS.
+ ///
+ public void EnableFilter() =>
+ settings.AdditionalSettings.TryAdd("filter", true);
+
+ ///
+ /// Enables the operation filter box, optionally making it pinnable.
+ ///
+ ///
+ /// If , the filter box can be pinned in place.
+ ///
+ public void EnableFilter(bool pinnable)
+ {
+ settings.EnableFilter();
+
+ if (pinnable)
+ settings.AdditionalSettings.EnablePinnableFilterBar();
+ }
+
///
/// Shows a button to scroll back to the top of the page.
///
diff --git a/src/NSwag.AspNetCore.Themes/NSwag.AspNetCore.Themes.csproj b/src/NSwag.AspNetCore.Themes/NSwag.AspNetCore.Themes.csproj
index 19caafd..a966f7d 100644
--- a/src/NSwag.AspNetCore.Themes/NSwag.AspNetCore.Themes.csproj
+++ b/src/NSwag.AspNetCore.Themes/NSwag.AspNetCore.Themes.csproj
@@ -1,17 +1,9 @@
ο»Ώ
+
-
-
- 3
- 0
- 2
-
- $(MajorVersion).$(MinorVersion).$(PatchVersion)$(VersionSuffix)
-
-
NSwag.AspNetCore.Themes
@@ -24,7 +16,7 @@
-
+
diff --git a/src/NSwag.AspNetCore.Themes/package-readme.md b/src/NSwag.AspNetCore.Themes/package-readme.md
index c59e707..c45ee78 100644
--- a/src/NSwag.AspNetCore.Themes/package-readme.md
+++ b/src/NSwag.AspNetCore.Themes/package-readme.md
@@ -21,6 +21,9 @@ app.UseSwaggerUi(Theme.Dark);
// Or enable runtime theme switcher!
app.UseSwaggerUi(Theme.Dark, c => c.EnableThemeSwitcher());
+
+// Or enable a pinnable filter box
+app.UseSwaggerUi(Theme.Dark, c => c.EnableFilter(pinnable: true));
```
> **Note**: The `UseSwaggerUi()` method is provided by NSwag.AspNetCore. This package adds convenient overloads to apply themes seamlessly.
diff --git a/src/NSwag.AspNetCore.Themes/readme.txt b/src/NSwag.AspNetCore.Themes/readme.txt
index e087d7e..8f83215 100644
--- a/src/NSwag.AspNetCore.Themes/readme.txt
+++ b/src/NSwag.AspNetCore.Themes/readme.txt
@@ -1,57 +1,36 @@
ο»Ώ+===================================================================================+
-| NSwag.AspNetCore.Themes β’ RELEASE NOTES - v3 |
+| NSwag.AspNetCore.Themes β’ RELEASE NOTES - v3.1 |
+===================================================================================+
-BREAKING CHANGES
--------------------------------------------------------------------------------------
-- API redesign: Style -> Theme (all classes renamed)
-- Classic themes removed (modern themes only)
-- .NET 6 & 7 support discontinued
-
-See migration guide: https://github.com/teociaps/SwaggerUI.Themes/wiki/Migration-v3
-
-
NEW FEATURES
-------------------------------------------------------------------------------------
-- π₯ Runtime Theme Switcher: Change themes on-the-fly without refreshing the page
-- Auto-Discovery: Custom themes automatically available in the theme switcher
-- Nested Folder Support: Organize your themes in subfolders
-- Standalone Themes: Create CSS-only themes with zero dependencies
-- Smart Filename Resolution: Support standard and standalone variants simultaneously
-- Minified Stylesheet Support: Optimize load times with minified CSS
-
-
-PLATFORM & DEPENDENCIES
--------------------------------------------------------------------------------------
-- .NET 10 support added
-- NSwag.AspNetCore updated to v14.6.3
+- Filter Box Support: settings.EnableFilter() is now available for NSwag, enabling
+ Swagger UI's native operation filter box
+- Pinnable Filter Bar: Pin the operation filter box in place, just like the topbar
+ Enable it with: settings.EnableFilter(pinnable: true);
IMPROVEMENTS
-------------------------------------------------------------------------------------
-- Unified modern theme system with consistent defaults
-- Advanced features now available on all themes
-- Smaller package footprint (optimized assets)
-- Improved performance and clarity throughout
+- Pinnable topbar now remembers its pinned/unpinned state across page reloads
+- Improved theme asset serving performance
+- Fixed a thread-safety issue that could affect parallel test execution
QUICK START
-------------------------------------------------------------------------------------
-Basic theme:
- app.UseSwaggerUi(Theme.Dark);
-
-Enable theme switcher:
- app.UseSwaggerUi(Theme.Dark, c => c.EnableThemeSwitcher());
+Filter box:
+ app.UseSwaggerUi(Theme.Dark, settings => settings.EnableFilter());
-All features:
- app.UseSwaggerUi(Theme.Dark, c => c.EnableAllAdvancedOptions());
+Pinnable filter bar:
+ app.UseSwaggerUi(Theme.Dark, settings => settings.EnableFilter(pinnable: true));
DOCUMENTATION
-------------------------------------------------------------------------------------
-Full docs: https://github.com/teociaps/SwaggerUI.Themes/wiki
-Migration: https://github.com/teociaps/SwaggerUI.Themes/wiki/Migration-v3
-Repository: https://github.com/teociaps/SwaggerUI.Themes
+Full docs: https://github.com/teociaps/SwaggerUI.Themes/wiki
+Troubleshooting: https://github.com/teociaps/SwaggerUI.Themes/wiki/Troubleshooting
+Repository: https://github.com/teociaps/SwaggerUI.Themes
=====================================================================================
\ No newline at end of file
diff --git a/tests/AspNetCore.Swagger.Extensions.Tests/AppendOperationCountToTagDescriptionFilterTests.cs b/tests/AspNetCore.Swagger.Extensions.Tests/AppendOperationCountToTagDescriptionFilterTests.cs
new file mode 100644
index 0000000..b6cc1f9
--- /dev/null
+++ b/tests/AspNetCore.Swagger.Extensions.Tests/AppendOperationCountToTagDescriptionFilterTests.cs
@@ -0,0 +1,233 @@
+using AspNetCore.Swagger.Extensions.Filters;
+using Microsoft.AspNetCore.Mvc.ApiExplorer;
+#if NET10_0_OR_GREATER
+using Microsoft.OpenApi;
+#else
+using Microsoft.OpenApi.Models;
+#endif
+using Shouldly;
+using Swashbuckle.AspNetCore.SwaggerGen;
+using System.Text.Json;
+using Xunit;
+
+namespace AspNetCore.Swagger.Extensions.Tests;
+
+public class AppendOperationCountToTagDescriptionFilterTests
+{
+ [Fact]
+ public void Constructor_WithValidTemplate_Succeeds()
+ {
+ // Act
+ var filter = new AppendOperationCountToTagDescriptionFilter(" (operations: {0})");
+
+ // Assert
+ filter.ShouldNotBeNull();
+ }
+
+ [Fact]
+ public void Constructor_WithNullTemplate_ThrowsArgumentException()
+ {
+ // Act & Assert
+ Should.Throw(() =>
+ new AppendOperationCountToTagDescriptionFilter(null!));
+ }
+
+ [Fact]
+ public void Constructor_WithEmptyTemplate_ThrowsArgumentException()
+ {
+ // Act & Assert
+ Should.Throw(() =>
+ new AppendOperationCountToTagDescriptionFilter(""));
+ }
+
+ [Fact]
+ public void Constructor_WithoutPlaceholder_ThrowsArgumentException()
+ {
+ // Act & Assert
+ var exception = Should.Throw(() =>
+ new AppendOperationCountToTagDescriptionFilter(" operations"));
+ exception.Message.ShouldContain("{0}");
+ }
+
+ [Fact]
+ public void Apply_WithEmptyDocument_DoesNothing()
+ {
+ // Arrange
+ var filter = new AppendOperationCountToTagDescriptionFilter();
+ var document = new OpenApiDocument
+ {
+ Paths = []
+ };
+ var context = CreateContext();
+
+ // Act
+ filter.Apply(document, context);
+
+ // Assert
+ // Tags is left untouched when there's nothing to append - its default depends on the
+ // Microsoft.OpenApi version (empty collection pre-v2, null from v2 onward), and either
+ // is correct here since the filter never had a reason to allocate one.
+ (document.Tags is null || document.Tags.Count == 0).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void Apply_WithOperations_AppendsCount()
+ {
+ // Arrange
+ var filter = new AppendOperationCountToTagDescriptionFilter();
+ var document = CreateDocumentWithTags();
+ var context = CreateContext();
+
+ // Act
+ filter.Apply(document, context);
+
+ // Assert
+ document.Tags.ShouldNotBeEmpty();
+ var userTag = document.Tags.FirstOrDefault(t => t.Name == "Users");
+ userTag.ShouldNotBeNull();
+ userTag.Description.ShouldContain("(operations: 2)");
+ }
+
+ [Fact]
+ public void Apply_WithCustomTemplate_UsesTemplate()
+ {
+ // Arrange
+ var filter = new AppendOperationCountToTagDescriptionFilter(" [{0} endpoints]");
+ var document = CreateDocumentWithTags();
+ var context = CreateContext();
+
+ // Act
+ filter.Apply(document, context);
+
+ // Assert
+ var userTag = document.Tags.FirstOrDefault(t => t.Name == "Users");
+ userTag.ShouldNotBeNull();
+ userTag.Description.ShouldContain("[2 endpoints]");
+ }
+
+ [Fact]
+ public void Apply_WithExistingDescription_AppendsCount()
+ {
+ // Arrange
+ var filter = new AppendOperationCountToTagDescriptionFilter();
+ var document = CreateDocumentWithTags();
+#if NET10_0_OR_GREATER
+ document.Tags = new HashSet
+ {
+ new() { Name = "Users", Description = "User management" }
+ };
+#else
+ document.Tags = new List
+ {
+ new() { Name = "Users", Description = "User management" }
+ };
+#endif
+ var context = CreateContext();
+
+ // Act
+ filter.Apply(document, context);
+
+ // Assert
+ var userTag = document.Tags.FirstOrDefault(t => t.Name == "Users");
+ userTag.ShouldNotBeNull();
+ userTag.Description.ShouldBe("User management (operations: 2)");
+ }
+
+ [Fact]
+ public void Apply_WithMultipleTags_CountsCorrectly()
+ {
+ // Arrange
+ var filter = new AppendOperationCountToTagDescriptionFilter();
+ var document = new OpenApiDocument
+ {
+ Paths = new OpenApiPaths
+ {
+ ["/users"] = new OpenApiPathItem
+ {
+#if NET10_0_OR_GREATER
+ Operations = new Dictionary
+ {
+ [HttpMethod.Get] = new() { Tags = new HashSet { new("Users", null) } },
+ [HttpMethod.Post] = new() { Tags = new HashSet { new("Users", null) } }
+ }
+#else
+ Operations = new Dictionary
+ {
+ [OperationType.Get] = new() { Tags = new List { new() { Name = "Users" } } },
+ [OperationType.Post] = new() { Tags = new List { new() { Name = "Users" } } }
+ }
+#endif
+ },
+ ["/products"] = new OpenApiPathItem
+ {
+#if NET10_0_OR_GREATER
+ Operations = new Dictionary
+ {
+ [HttpMethod.Get] = new() { Tags = new HashSet { new("Products", null) } }
+ }
+#else
+ Operations = new Dictionary
+ {
+ [OperationType.Get] = new() { Tags = new List { new() { Name = "Products" } } }
+ }
+#endif
+ }
+ }
+ };
+ var context = CreateContext();
+
+ // Act
+ filter.Apply(document, context);
+
+ // Assert
+ document.Tags.Count.ShouldBe(2);
+ document.Tags.First(t => t.Name == "Users").Description.ShouldContain("(operations: 2)");
+ document.Tags.First(t => t.Name == "Products").Description.ShouldContain("(operations: 1)");
+ }
+
+ private static DocumentFilterContext CreateContext()
+ {
+ return new DocumentFilterContext(
+ new List(),
+ new SchemaGenerator(new SchemaGeneratorOptions(), new JsonSerializerDataContractResolver(new JsonSerializerOptions())),
+ new SchemaRepository());
+ }
+
+ private static OpenApiDocument CreateDocumentWithTags()
+ {
+ return new OpenApiDocument
+ {
+ Paths = new OpenApiPaths
+ {
+ ["/users"] = new OpenApiPathItem
+ {
+#if NET10_0_OR_GREATER
+ Operations = new Dictionary
+ {
+ [HttpMethod.Get] = new()
+ {
+ Tags = new HashSet { new("Users", null) }
+ },
+ [HttpMethod.Post] = new()
+ {
+ Tags = new HashSet { new("Users", null) }
+ }
+ }
+#else
+ Operations = new Dictionary
+ {
+ [OperationType.Get] = new()
+ {
+ Tags = new List { new() { Name = "Users" } }
+ },
+ [OperationType.Post] = new()
+ {
+ Tags = new List { new() { Name = "Users" } }
+ }
+ }
+#endif
+ }
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/tests/AspNetCore.Swagger.Extensions.Tests/AppendOperationCountToTagDescriptionProcessorTests.cs b/tests/AspNetCore.Swagger.Extensions.Tests/AppendOperationCountToTagDescriptionProcessorTests.cs
new file mode 100644
index 0000000..3f4b3d5
--- /dev/null
+++ b/tests/AspNetCore.Swagger.Extensions.Tests/AppendOperationCountToTagDescriptionProcessorTests.cs
@@ -0,0 +1,182 @@
+using AspNetCore.Swagger.Extensions.Processors;
+using NSwag;
+using NSwag.Generation.Processors.Contexts;
+using Shouldly;
+using Xunit;
+
+namespace AspNetCore.Swagger.Extensions.Tests;
+
+public class AppendOperationCountToTagDescriptionProcessorTests
+{
+ [Fact]
+ public void Constructor_WithValidTemplate_Succeeds()
+ {
+ // Act
+ var processor = new AppendOperationCountToTagDescriptionProcessor(" (operations: {0})");
+
+ // Assert
+ processor.ShouldNotBeNull();
+ }
+
+ [Fact]
+ public void Constructor_WithNullTemplate_ThrowsArgumentException()
+ {
+ // Act & Assert
+ Should.Throw(() =>
+ new AppendOperationCountToTagDescriptionProcessor(null!));
+ }
+
+ [Fact]
+ public void Constructor_WithEmptyTemplate_ThrowsArgumentException()
+ {
+ // Act & Assert
+ Should.Throw(() =>
+ new AppendOperationCountToTagDescriptionProcessor(""));
+ }
+
+ [Fact]
+ public void Constructor_WithoutPlaceholder_ThrowsArgumentException()
+ {
+ // Act & Assert
+ var exception = Should.Throw(() =>
+ new AppendOperationCountToTagDescriptionProcessor(" operations"));
+ exception.Message.ShouldContain("{0}");
+ }
+
+ [Fact]
+ public void Process_WithEmptyDocument_DoesNothing()
+ {
+ // Arrange
+ var processor = new AppendOperationCountToTagDescriptionProcessor();
+ var document = new OpenApiDocument
+ {
+ Tags = []
+ };
+ var context = CreateContext(document);
+
+ // Act
+ processor.Process(context);
+
+ // Assert
+ document.Tags.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public void Process_WithOperations_AppendsCount()
+ {
+ // Arrange
+ var processor = new AppendOperationCountToTagDescriptionProcessor();
+ var document = CreateDocumentWithTags();
+ var context = CreateContext(document);
+
+ // Act
+ processor.Process(context);
+
+ // Assert
+ document.Tags.ShouldNotBeEmpty();
+ var userTag = document.Tags.FirstOrDefault(t => t.Name == "Users");
+ userTag.ShouldNotBeNull();
+ userTag.Description.ShouldContain("(operations: 2)");
+ }
+
+ [Fact]
+ public void Process_WithCustomTemplate_UsesTemplate()
+ {
+ // Arrange
+ var processor = new AppendOperationCountToTagDescriptionProcessor(" [{0} endpoints]");
+ var document = CreateDocumentWithTags();
+ var context = CreateContext(document);
+
+ // Act
+ processor.Process(context);
+
+ // Assert
+ var userTag = document.Tags.FirstOrDefault(t => t.Name == "Users");
+ userTag.ShouldNotBeNull();
+ userTag.Description.ShouldContain("[2 endpoints]");
+ }
+
+ [Fact]
+ public void Process_WithExistingDescription_AppendsCount()
+ {
+ // Arrange
+ var processor = new AppendOperationCountToTagDescriptionProcessor();
+ var document = CreateDocumentWithTags();
+ document.Tags = new List
+ {
+ new() { Name = "Users", Description = "User management" }
+ };
+ var context = CreateContext(document);
+
+ // Act
+ processor.Process(context);
+
+ // Assert
+ var userTag = document.Tags.FirstOrDefault(t => t.Name == "Users");
+ userTag.ShouldNotBeNull();
+ userTag.Description.ShouldBe("User management (operations: 2)");
+ }
+
+ [Fact]
+ public void Process_WithMultipleTags_CountsCorrectly()
+ {
+ // Arrange
+ var processor = new AppendOperationCountToTagDescriptionProcessor();
+ var document = new OpenApiDocument();
+ document.Paths.Add("/users", new OpenApiPathItem
+ {
+ {
+ OpenApiOperationMethod.Get,
+ new OpenApiOperation { Tags = new List { "Users" } }
+ },
+ {
+ OpenApiOperationMethod.Post,
+ new OpenApiOperation { Tags = new List { "Users" } }
+ }
+ });
+ document.Paths.Add("/products", new OpenApiPathItem
+ {
+ {
+ OpenApiOperationMethod.Get,
+ new OpenApiOperation { Tags = new List { "Products" } }
+ }
+ });
+ var context = CreateContext(document);
+
+ // Act
+ processor.Process(context);
+
+ // Assert
+ document.Tags.Count.ShouldBe(2);
+ document.Tags.First(t => t.Name == "Users").Description.ShouldContain("(operations: 2)");
+ document.Tags.First(t => t.Name == "Products").Description.ShouldContain("(operations: 1)");
+ }
+
+ private static DocumentProcessorContext CreateContext(OpenApiDocument document)
+ {
+ return new DocumentProcessorContext(
+ document,
+ new List(),
+ new List(),
+ null!,
+ null!,
+ null!);
+ }
+
+ private static OpenApiDocument CreateDocumentWithTags()
+ {
+ var document = new OpenApiDocument();
+ document.Paths.Add("/users", new OpenApiPathItem
+ {
+ {
+ OpenApiOperationMethod.Get,
+ new OpenApiOperation { Tags = new List { "Users" } }
+ },
+ {
+ OpenApiOperationMethod.Post,
+ new OpenApiOperation { Tags = new List { "Users" } }
+ }
+ });
+ return document;
+ }
+}
\ No newline at end of file
diff --git a/tests/AspNetCore.Swagger.Extensions.Tests/AspNetCore.Swagger.Extensions.Tests.csproj b/tests/AspNetCore.Swagger.Extensions.Tests/AspNetCore.Swagger.Extensions.Tests.csproj
new file mode 100644
index 0000000..5423612
--- /dev/null
+++ b/tests/AspNetCore.Swagger.Extensions.Tests/AspNetCore.Swagger.Extensions.Tests.csproj
@@ -0,0 +1,41 @@
+ο»Ώ
+
+
+
+
+ false
+ true
+ $(MSBuildProjectName.Replace(" ", "_"))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/AspNetCore.Swagger.Extensions.Tests/AspNetCoreOpenApiDocumentGeneratorSettingsExtensionsTests.cs b/tests/AspNetCore.Swagger.Extensions.Tests/AspNetCoreOpenApiDocumentGeneratorSettingsExtensionsTests.cs
new file mode 100644
index 0000000..6fdd55a
--- /dev/null
+++ b/tests/AspNetCore.Swagger.Extensions.Tests/AspNetCoreOpenApiDocumentGeneratorSettingsExtensionsTests.cs
@@ -0,0 +1,42 @@
+using AspNetCore.Swagger.Extensions.Processors;
+using Microsoft.Extensions.DependencyInjection;
+using NSwag.Generation.AspNetCore;
+using Shouldly;
+using Xunit;
+
+namespace AspNetCore.Swagger.Extensions.Tests;
+
+public class AspNetCoreOpenApiDocumentGeneratorSettingsExtensionsTests
+{
+ [Fact]
+ public void AppendOperationCountToTags_AddsProcessor()
+ {
+ // Arrange
+ var settings = new AspNetCoreOpenApiDocumentGeneratorSettings();
+
+ // Act
+ settings.AppendOperationCountToTags();
+
+ // Assert
+ settings.DocumentProcessors.ShouldNotBeEmpty();
+ settings.DocumentProcessors.ShouldContain(p =>
+ p.GetType() == typeof(AppendOperationCountToTagDescriptionProcessor));
+ }
+
+ [Fact]
+ public void AppendOperationCountToTags_WithCustomTemplate_AddsProcessorWithTemplate()
+ {
+ // Arrange
+ var settings = new AspNetCoreOpenApiDocumentGeneratorSettings();
+ var customTemplate = " [{0} endpoints]";
+
+ // Act
+ settings.AppendOperationCountToTags(customTemplate);
+
+ // Assert
+ settings.DocumentProcessors.ShouldNotBeEmpty();
+ var processor = settings.DocumentProcessors
+ .FirstOrDefault(p => p.GetType() == typeof(AppendOperationCountToTagDescriptionProcessor));
+ processor.ShouldNotBeNull();
+ }
+}
\ No newline at end of file
diff --git a/tests/AspNetCore.Swagger.Extensions.Tests/SwaggerGenOptionsExtensionsTests.cs b/tests/AspNetCore.Swagger.Extensions.Tests/SwaggerGenOptionsExtensionsTests.cs
new file mode 100644
index 0000000..610b7e6
--- /dev/null
+++ b/tests/AspNetCore.Swagger.Extensions.Tests/SwaggerGenOptionsExtensionsTests.cs
@@ -0,0 +1,43 @@
+using AspNetCore.Swagger.Extensions.Filters;
+using Microsoft.Extensions.DependencyInjection;
+using Shouldly;
+using Swashbuckle.AspNetCore.SwaggerGen;
+using Xunit;
+
+namespace AspNetCore.Swagger.Extensions.Tests;
+
+public class SwaggerGenOptionsExtensionsTests
+{
+ [Fact]
+ public void AppendOperationCountToTags_AddsFilter()
+ {
+ // Arrange
+ var options = new SwaggerGenOptions();
+
+ // Act
+ options.AppendOperationCountToTags();
+
+ // Assert
+ options.DocumentFilterDescriptors.ShouldNotBeEmpty();
+ options.DocumentFilterDescriptors.ShouldContain(d =>
+ d.Type == typeof(AppendOperationCountToTagDescriptionFilter));
+ }
+
+ [Fact]
+ public void AppendOperationCountToTags_WithCustomTemplate_AddsFilterWithTemplate()
+ {
+ // Arrange
+ var options = new SwaggerGenOptions();
+ var customTemplate = " [{0} endpoints]";
+
+ // Act
+ options.AppendOperationCountToTags(customTemplate);
+
+ // Assert
+ options.DocumentFilterDescriptors.ShouldNotBeEmpty();
+ var descriptor = options.DocumentFilterDescriptors
+ .FirstOrDefault(d => d.Type == typeof(AppendOperationCountToTagDescriptionFilter));
+ descriptor.ShouldNotBeNull();
+ descriptor.Arguments.ShouldContain(customTemplate);
+ }
+}
\ No newline at end of file
diff --git a/tests/AspNetCore.SwaggerUI.Themes.Tests/AdvancedOptionsTests.cs b/tests/AspNetCore.SwaggerUI.Themes.Tests/AdvancedOptionsTests.cs
index 49ad878..8e001f7 100644
--- a/tests/AspNetCore.SwaggerUI.Themes.Tests/AdvancedOptionsTests.cs
+++ b/tests/AspNetCore.SwaggerUI.Themes.Tests/AdvancedOptionsTests.cs
@@ -20,6 +20,20 @@ public void EnablePinnableTopbar_AddsCorrectOption()
options[AdvancedOptions.PinnableTopbar].ShouldBe(true);
}
+ [Fact]
+ public void EnablePinnableFilterBar_AddsCorrectOption()
+ {
+ // Arrange
+ var options = new Dictionary();
+
+ // Act
+ options.EnablePinnableFilterBar();
+
+ // Assert
+ options.ShouldContainKey(AdvancedOptions.PinnableFilterBar);
+ options[AdvancedOptions.PinnableFilterBar].ShouldBe(true);
+ }
+
[Fact]
public void EnableBackToTop_AddsCorrectOption()
{
@@ -114,12 +128,14 @@ public void Apply_AppliesJsFeaturesCorrectly()
// Arrange
const string JsContent = $"""
const pinnableTopbapFeature = {AdvancedOptions.PinnableTopbarJsPH};
+ const pinnableFilterBarFeature = {AdvancedOptions.PinnableFilterBarJsPH};
const backToTopFeature = {AdvancedOptions.BackToTopJsPH};
const expandOrCollapseOpsFeature = {AdvancedOptions.ExpandOrCollapseAllOperationsJsPH};
""";
var options = new Dictionary
{
{ AdvancedOptions.PinnableTopbar, true },
+ { AdvancedOptions.PinnableFilterBar, true },
{ AdvancedOptions.BackToTop, true },
{ AdvancedOptions.ExpandOrCollapseAllOperations, true }
};
@@ -129,6 +145,7 @@ public void Apply_AppliesJsFeaturesCorrectly()
const string ExpectedJsConent = """
const pinnableTopbapFeature = true;
+ const pinnableFilterBarFeature = true;
const backToTopFeature = true;
const expandOrCollapseOpsFeature = true;
""";
@@ -136,6 +153,7 @@ public void Apply_AppliesJsFeaturesCorrectly()
// Assert
result.ShouldBe(ExpectedJsConent);
result.ShouldNotContain(AdvancedOptions.PinnableTopbarJsPH);
+ result.ShouldNotContain(AdvancedOptions.PinnableFilterBarJsPH);
}
[Fact]
diff --git a/tests/AspNetCore.SwaggerUI.Themes.Tests/AspNetCore.Swagger.Themes.Tests.csproj b/tests/AspNetCore.SwaggerUI.Themes.Tests/AspNetCore.Swagger.Themes.Tests.csproj
index 45ffea8..992da6f 100644
--- a/tests/AspNetCore.SwaggerUI.Themes.Tests/AspNetCore.Swagger.Themes.Tests.csproj
+++ b/tests/AspNetCore.SwaggerUI.Themes.Tests/AspNetCore.Swagger.Themes.Tests.csproj
@@ -20,13 +20,13 @@
-
+
-
+
@@ -34,15 +34,17 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
+
+
diff --git a/tests/AspNetCore.SwaggerUI.Themes.Tests/FileProviderMiddlewareTests.cs b/tests/AspNetCore.SwaggerUI.Themes.Tests/FileProviderMiddlewareTests.cs
index 89f1aac..c76c1f6 100644
--- a/tests/AspNetCore.SwaggerUI.Themes.Tests/FileProviderMiddlewareTests.cs
+++ b/tests/AspNetCore.SwaggerUI.Themes.Tests/FileProviderMiddlewareTests.cs
@@ -114,4 +114,96 @@ public async Task AddGetEndpoint_ShouldHandleCustomContentType()
var responseBody = await reader.ReadToEndAsync();
responseBody.ShouldBe(content);
}
+
+ [Fact]
+ public async Task AddGetEndpoint_ShouldResolveEachPathIndependently_WhenMultiplePathsRegisteredOnSameApp()
+ {
+ // Arrange
+ var mockAppBuilder = new MockApplicationBuilder();
+ var prefix = $"/test-middleware-multi-{Guid.NewGuid():N}";
+ var registrations = Enumerable.Range(0, 6)
+ .Select(i => (
+ Path: $"{prefix}/{i}.css",
+ Content: $"body {{ /* asset {i} */ }}",
+ ContentType: i % 2 == 0 ? MimeTypes.Text.Css : MimeTypes.Text.Javascript))
+ .ToList();
+
+ foreach (var (path, content, contentType) in registrations)
+ {
+ AddGetEndpoint(mockAppBuilder, path, content, contentType);
+ }
+
+ var app = mockAppBuilder.Build();
+
+ // Act & Assert - each registered path resolves to its own content and content-type
+ foreach (var (path, content, contentType) in registrations)
+ {
+ var context = MockApplicationBuilder.CreateHttpContext(path);
+ await app.Invoke(context);
+ await context.Response.Body.FlushAsync();
+
+ context.Response.StatusCode.ShouldBe(200);
+ context.Response.ContentType.ShouldBe(contentType);
+
+ context.Response.Body.Seek(0, SeekOrigin.Begin);
+ using var reader = new StreamReader(context.Response.Body);
+ (await reader.ReadToEndAsync()).ShouldBe(content);
+ }
+
+ // An unregistered path still falls through to the terminal 404
+ var missingContext = MockApplicationBuilder.CreateHttpContext($"{prefix}/unregistered.css");
+ await app.Invoke(missingContext);
+
+ missingContext.Response.StatusCode.ShouldBe(404);
+ }
+
+ [Fact]
+ public async Task AddGetEndpoint_ShouldNotServePathRegisteredByAnotherAppInstance()
+ {
+ // Arrange - two independent apps, each registering its own, distinct path
+ var appA = new MockApplicationBuilder();
+ var appB = new MockApplicationBuilder();
+ var prefix = $"/test-middleware-isolation-{Guid.NewGuid():N}";
+ var pathOnlyRegisteredByA = $"{prefix}/a.css";
+ var pathOnlyRegisteredByB = $"{prefix}/b.css";
+
+ AddGetEndpoint(appA, pathOnlyRegisteredByA, "content-A");
+ AddGetEndpoint(appB, pathOnlyRegisteredByB, "content-B");
+
+ var builtA = appA.Build();
+
+ // Act - request appA for a path only appB ever registered
+ var context = MockApplicationBuilder.CreateHttpContext(pathOnlyRegisteredByB);
+ await builtA.Invoke(context);
+
+ // Assert - appA never registered this path, so it must not serve appB's content
+ context.Response.StatusCode.ShouldBe(404);
+ }
+
+ [Fact]
+ public async Task AddGetEndpoint_WhenCalledConcurrently_RegistersEveryEndpointExactlyOnce()
+ {
+ // Arrange
+ const int EndpointCount = 50;
+ var prefix = $"/test-middleware-concurrent-{Guid.NewGuid():N}";
+ var registrations = Enumerable.Range(0, EndpointCount)
+ .Select(i => (Path: $"{prefix}/{i}.css", AppBuilder: new MockApplicationBuilder()))
+ .ToList();
+
+ // Act - each registration uses its own builder so the middleware pipeline itself
+ // isn't mutated concurrently; only the shared static endpoint registry is stressed.
+ await Should.NotThrowAsync(() => Task.WhenAll(
+ registrations.Select(r => Task.Run(() =>
+ AddGetEndpoint(r.AppBuilder, r.Path, $"body {{ /* {r.Path} */ }}")))));
+
+ // Assert
+ foreach (var (path, appBuilder) in registrations)
+ {
+ var app = appBuilder.Build();
+ var context = MockApplicationBuilder.CreateHttpContext(path);
+ await app.Invoke(context);
+
+ context.Response.StatusCode.ShouldBe(200);
+ }
+ }
}
\ No newline at end of file
diff --git a/tests/AspNetCore.SwaggerUI.Themes.Tests/SwaggerUIOptionsExtensionsTests.cs b/tests/AspNetCore.SwaggerUI.Themes.Tests/SwaggerUIOptionsExtensionsTests.cs
new file mode 100644
index 0000000..0513074
--- /dev/null
+++ b/tests/AspNetCore.SwaggerUI.Themes.Tests/SwaggerUIOptionsExtensionsTests.cs
@@ -0,0 +1,35 @@
+using Microsoft.AspNetCore.Builder;
+using Shouldly;
+using Swashbuckle.AspNetCore.SwaggerUI;
+using Xunit;
+
+namespace AspNetCore.Swagger.Themes.Tests;
+
+public class SwaggerUIOptionsExtensionsTests
+{
+ [Fact]
+ public void EnableFilter_Pinnable_AddsPinnableFilterBarOption()
+ {
+ // Arrange
+ var options = new SwaggerUIOptions();
+
+ // Act
+ options.EnableFilter(pinnable: true);
+
+ // Assert
+ options.ConfigObject.AdditionalItems.ShouldContainKey(AdvancedOptions.PinnableFilterBar);
+ }
+
+ [Fact]
+ public void EnableFilter_NotPinnable_DoesNotAddPinnableFilterBarOption()
+ {
+ // Arrange
+ var options = new SwaggerUIOptions();
+
+ // Act
+ options.EnableFilter(pinnable: false);
+
+ // Assert
+ options.ConfigObject.AdditionalItems.ShouldNotContainKey(AdvancedOptions.PinnableFilterBar);
+ }
+}
diff --git a/tests/AspNetCore.SwaggerUI.Themes.Tests/ThemeProviderTests.cs b/tests/AspNetCore.SwaggerUI.Themes.Tests/ThemeProviderTests.cs
index 91edd37..d00ef80 100644
--- a/tests/AspNetCore.SwaggerUI.Themes.Tests/ThemeProviderTests.cs
+++ b/tests/AspNetCore.SwaggerUI.Themes.Tests/ThemeProviderTests.cs
@@ -1,13 +1,22 @@
using AspNetCore.Swagger.Themes.Tests.Utilities;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
using Shouldly;
+using System.Net;
using System.Reflection;
+using System.Text.Encodings.Web;
using static AspNetCore.Swagger.Themes.FileProvider;
namespace AspNetCore.Swagger.Themes.Tests;
///
-/// Tests for ThemeProvider functionality using WebApplicationFactory.
-/// Tests for non-WebApplication scenarios are in FileProviderMiddlewareTests.
+/// Tests for ThemeProvider functionality using a WebApplication host (via WebApplicationFactory
+/// or a locally hosted TestServer). Tests for non-WebApplication scenarios are in FileProviderMiddlewareTests.
///
public class ThemeProviderTests : IClassFixture>
{
@@ -177,6 +186,91 @@ public async Task AddGetEndpoint_ShouldReturnStyleContent_WhenWebApplication(Bas
(await response.Content.ReadAsStringAsync()).ShouldBeEquivalentTo(styleText);
}
+ [Fact]
+ public async Task AddGetEndpoint_ShouldResolveEachRegisteredPath_WhenWebApplication()
+ {
+ // Arrange - the shared test WebApplication (see Program.cs) registers one path per
+ // theme via AddGetEndpoint at startup, all served through the app's routing table.
+ var registeredThemes = new ThemeTestData();
+
+ // Act & Assert - each theme's style path resolves independently through the same WebApplication instance
+ foreach (var theme in registeredThemes)
+ {
+ var fullPath = StylesPath + theme.FileName;
+ var expectedContent = GetResourceText(theme.FileName, theme.GetType());
+
+ var response = await _themeProviderWebApplicationFactory.Client.GetAsync(fullPath);
+
+ response.StatusCode.ShouldBe(HttpStatusCode.OK);
+ response.Content.Headers.ContentType.MediaType.ShouldBe(MimeTypes.Text.Css);
+ (await response.Content.ReadAsStringAsync()).ShouldBeEquivalentTo(expectedContent);
+ }
+
+ // An unregistered path still falls through to the app's default 404 handling
+ var missingResponse = await _themeProviderWebApplicationFactory.Client.GetAsync(
+ $"{StylesPath}unregistered-{Guid.NewGuid():N}.css");
+
+ missingResponse.StatusCode.ShouldBe(HttpStatusCode.NotFound);
+ }
+
+ [Fact]
+ public async Task AddGetEndpoint_ShouldBypassGlobalFallbackAuthorizationPolicy_WhenWebApplication()
+ {
+ // Arrange - a WebApplication secured with a global fallback policy requiring an
+ // authenticated user, mirroring a consumer who secures everything by default
+ // before calling UseSwaggerUI().
+ var builder = WebApplication.CreateBuilder();
+ builder.WebHost.UseTestServer();
+ builder.Services
+ .AddAuthentication(DenyAllAuthenticationHandler.SchemeName)
+ .AddScheme(DenyAllAuthenticationHandler.SchemeName, null);
+ builder.Services.AddAuthorization(options =>
+ options.FallbackPolicy = new AuthorizationPolicyBuilder(DenyAllAuthenticationHandler.SchemeName)
+ .RequireAuthenticatedUser()
+ .Build());
+
+ await using var app = builder.Build();
+ app.UseAuthentication();
+ app.UseAuthorization();
+
+ const string ThemePath = "/styles/auth-bypass-theme.css";
+ const string ThemeContent = "body { }";
+ AddGetEndpoint(app, ThemePath, ThemeContent);
+
+ // A control endpoint mapped without AllowAnonymous proves the fallback policy is actually enforced
+ app.MapGet("/secured-control", () => "secured");
+
+ await app.StartAsync();
+ var client = app.GetTestClient();
+
+ // Act
+ var themeResponse = await client.GetAsync(ThemePath);
+ var controlResponse = await client.GetAsync("/secured-control");
+
+ // Assert - the theme asset bypasses the fallback policy via AllowAnonymous; the control endpoint doesn't
+ themeResponse.StatusCode.ShouldBe(HttpStatusCode.OK);
+ (await themeResponse.Content.ReadAsStringAsync()).ShouldBe(ThemeContent);
+
+ controlResponse.StatusCode.ShouldNotBe(HttpStatusCode.OK);
+
+ await app.StopAsync();
+ }
+
+ ///
+ /// Authentication handler that never authenticates the current request, used to
+ /// exercise a global fallback authorization policy without depending on a real scheme.
+ ///
+ private sealed class DenyAllAuthenticationHandler(
+ IOptionsMonitor options,
+ ILoggerFactory logger,
+ UrlEncoder encoder) : AuthenticationHandler(options, logger, encoder)
+ {
+ public const string SchemeName = "DenyAll";
+
+ protected override Task HandleAuthenticateAsync() =>
+ Task.FromResult(AuthenticateResult.NoResult());
+ }
+
[Fact]
public void GetResourceText_ShouldFindThemeInSubfolder()
{
diff --git a/tests/AspNetCore.SwaggerUI.Themes.Tests/ThemeSwitcherTests.cs b/tests/AspNetCore.SwaggerUI.Themes.Tests/ThemeSwitcherTests.cs
index 2832b76..f6319b2 100644
--- a/tests/AspNetCore.SwaggerUI.Themes.Tests/ThemeSwitcherTests.cs
+++ b/tests/AspNetCore.SwaggerUI.Themes.Tests/ThemeSwitcherTests.cs
@@ -484,5 +484,33 @@ public void Scenario_CustomThemeModes_WorkCorrectly()
#endregion Integration Scenarios
+ #region Concurrency Tests
+
+ [Fact]
+ public async Task RegisterTheme_WhenCalledConcurrently_RegistersEveryThemeExactlyOnce()
+ {
+ // Arrange
+ const int ThemeCount = 50;
+ var prefix = $"concurrent-{Guid.NewGuid():N}-";
+ var themes = Enumerable.Range(0, ThemeCount)
+ .Select(i => new TestCustomTheme($"{prefix}{i}.css"))
+ .ToList();
+
+ // Act
+ await Should.NotThrowAsync(() => Task.WhenAll(
+ themes.Select(theme => Task.Run(() => ThemeSwitcher.RegisterTheme(theme, theme.FileName, isStandalone: false)))));
+
+ // Assert
+ var registeredNames = ThemeSwitcher.GetFilteredThemes(ThemeSwitcherOptions.CustomOnly())
+ .Select(rt => rt.Name)
+ .Where(name => name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ .ToList();
+
+ registeredNames.Count.ShouldBe(ThemeCount);
+ registeredNames.Distinct(StringComparer.OrdinalIgnoreCase).Count().ShouldBe(ThemeCount);
+ }
+
+ #endregion Concurrency Tests
+
private class TestCustomTheme(string fileName) : Theme(fileName);
}
\ No newline at end of file