├── .config
└── dotnet-tools.json
├── .editorconfig
├── .gitattributes
├── .github
├── CODEOWNERS
├── CONTRIBUTING.md
├── FUNDING.yml
├── ISSUE_TEMPLATE.md
├── ISSUE_TEMPLATE
│ ├── 10_bug_report.yml
│ ├── 20_feature_request.yml
│ ├── 30_question.yml
│ └── config.yml
├── PULL_REQUEST_TEMPLATE.md
├── actionlint-matcher.json
├── dependabot.yml
├── update-dotnet-sdk.json
└── workflows
│ ├── benchmark.yml
│ ├── build.yml
│ ├── bump-version.yml
│ ├── code-scan.yml
│ ├── dependency-review.yml
│ ├── lint.yml
│ ├── ossf-scorecard.yml
│ └── release.yml
├── .gitignore
├── .markdownlint.json
├── .vscode
├── extensions.json
├── launch.json
├── settings.json
└── tasks.json
├── .vsconfig
├── CODE_OF_CONDUCT.md
├── Directory.Build.props
├── Directory.Packages.props
├── LICENSE
├── NuGet.config
├── OpenApi.Extensions.ruleset
├── OpenApi.Extensions.slnx
├── README.md
├── SECURITY.MD
├── benchmark.ps1
├── build.ps1
├── exclusion.dic
├── global.json
├── omnisharp.json
├── package-icon.png
├── package-readme.md
├── perf
└── OpenApi.Extensions.Benchmarks
│ ├── MartinCostello.OpenApi.Extensions.Benchmarks.csproj
│ ├── OpenApiBenchmarks.cs
│ ├── Program.cs
│ ├── Properties
│ └── launchSettings.json
│ └── TodoAppServer.cs
├── samples
└── TodoApp
│ ├── ApiEndpoints.cs
│ ├── Data
│ ├── ITodoRepository.cs
│ ├── TodoContext.cs
│ ├── TodoItem.cs
│ └── TodoRepository.cs
│ ├── DateTimeExampleProvider.cs
│ ├── Extensions
│ └── DbSetExtensions.cs
│ ├── GuidExampleProvider.cs
│ ├── Models
│ ├── CreateTodoItemModel.cs
│ ├── CreatedTodoItemModel.cs
│ ├── TodoItemFilterModel.cs
│ ├── TodoItemModel.cs
│ └── TodoListViewModel.cs
│ ├── ProblemDetailsExampleProvider.cs
│ ├── Program.cs
│ ├── Properties
│ └── launchSettings.json
│ ├── Services
│ ├── ITodoService.cs
│ └── TodoService.cs
│ ├── TodoApp.csproj
│ ├── TodoAppBuilder.cs
│ ├── TodoJsonSerializerContext.cs
│ ├── appsettings.Development.json
│ ├── appsettings.json
│ └── wwwroot
│ ├── favicon.ico
│ └── swagger-ui
│ └── index.html
├── src
└── OpenApi.Extensions
│ ├── ExampleFormatter.cs
│ ├── ExampleFormatter.net9.0.cs
│ ├── ExampleTargets.cs
│ ├── IExampleProvider`1.cs
│ ├── IOpenApiExampleMetadata.cs
│ ├── IOpenApiExampleMetadata.net9.0.cs
│ ├── MartinCostello.OpenApi.Extensions.csproj
│ ├── OpenApiConstants.cs
│ ├── OpenApiEndpointConventionBuilderExtensions.cs
│ ├── OpenApiEndpointRouteBuilderExtensions.cs
│ ├── OpenApiExampleAttribute.cs
│ ├── OpenApiExampleAttribute`1.cs
│ ├── OpenApiExampleAttribute`2.cs
│ ├── OpenApiExampleAttribute`2.net9.0.cs
│ ├── OpenApiExtensions.cs
│ ├── OpenApiExtensionsOptions.cs
│ ├── OpenApiResponseAttribute.cs
│ ├── PublicAPI
│ ├── PublicAPI.Shipped.txt
│ ├── PublicAPI.Unshipped.txt
│ └── net9.0
│ │ ├── PublicAPI.Shipped.txt
│ │ └── PublicAPI.Unshipped.txt
│ ├── ReflectionExtensions.cs
│ ├── Transformers
│ ├── AddExamplesTransformer.cs
│ ├── AddOperationXmlDocumentationTransformer.cs
│ ├── AddParameterDescriptionsTransformer.cs
│ ├── AddResponseDescriptionsTransformer.cs
│ ├── AddSchemaXmlDocumentationTransformer.cs
│ ├── AddServersTransformer.cs
│ └── DescriptionsTransformer.cs
│ ├── XmlCommentsHelper.cs
│ └── XmlDescriptionService.cs
├── startvs.cmd
├── startvscode.cmd
├── stylecop.json
└── tests
├── Models.A
├── Animal.cs
├── AnimalExampleProvider.cs
├── AnimalsJsonSerializationContext.cs
├── Cat.cs
├── CatExampleProvider.cs
├── Dog.cs
├── DogExampleProvider.cs
├── IAnimal.cs
├── Models.A.csproj
├── Spot.cs
└── Tom.cs
├── Models.B
├── Car.cs
├── CarType.cs
├── Models.B.csproj
├── Motorcycle.cs
├── Vehicle.cs
└── VehiclesJsonSerializationContext.cs
├── OpenApi.Extensions.Tests
├── AssemblyTests.cs
├── ControllerTests.Schema_Is_Correct_For_Web_Api.DotNet9_0.verified.txt
├── ControllerTests.cs
├── DocumentTests.cs
├── ExampleFormatterTests.Can_Serialize_Boolean_value=False.DotNet9_0.verified.txt
├── ExampleFormatterTests.Can_Serialize_Boolean_value=True.DotNet9_0.verified.txt
├── ExampleFormatterTests.Can_Serialize_Complex_Object.DotNet9_0.verified.txt
├── ExampleFormatterTests.Can_Serialize_Integer.DotNet9_0.verified.txt
├── ExampleFormatterTests.Can_Serialize_Long.DotNet9_0.verified.txt
├── ExampleFormatterTests.Can_Serialize_Null_String.DotNet9_0.verified.txt
├── ExampleFormatterTests.Can_Serialize_String.DotNet9_0.verified.txt
├── ExampleFormatterTests.cs
├── IWebHostBuilderExtensions.cs
├── IntegrationTests.Http_404_Is_Returned_If_Yaml_Document_Not_Found.verified.txt
├── IntegrationTests.Schema_Is_Correct_For_App.DotNet9_0.verified.txt
├── IntegrationTests.Schema_Is_Correct_For_App_As_Yaml.DotNet9_0.verified.txt
├── IntegrationTests.Schema_Is_Correct_For_Classes.DotNet9_0.verified.txt
├── IntegrationTests.Schema_Is_Correct_For_Example_Attribute_Hierarchy.DotNet9_0.verified.txt
├── IntegrationTests.Schema_Is_Correct_For_Interfaces.DotNet9_0.verified.txt
├── IntegrationTests.Schema_Is_Correct_For_Not_Json.DotNet9_0.verified.txt
├── IntegrationTests.Schema_Is_Correct_For_Records.DotNet9_0.verified.txt
├── IntegrationTests.Schema_Is_Correct_For_Sample.DotNet9_0.verified.txt
├── IntegrationTests.cs
├── MartinCostello.OpenApi.Extensions.Tests.csproj
├── MinimalFixture.cs
├── MvcFixture.cs
├── OpenApiEndpointConventionBuilderExtensionsTests.cs
├── OpenApiExampleAttributeTests.cs
├── OpenApiExtensionsTests.cs
├── OpenApiResponseAttributeTests.cs
├── WebApplicationFactoryExtensions.cs
└── XmlCommentsHelperTests.cs
├── TestApp
├── AppJsonSerializationContext.cs
├── Greeting.cs
├── ProblemDetailsExampleProvider.cs
├── Program.cs
├── Properties
│ └── launchSettings.json
└── TestApp.csproj
└── WebApi
├── Controllers
└── TimeController.cs
├── Models
└── TimeModel.cs
├── MvcSerializerContext.cs
├── Program.cs
├── Properties
└── launchSettings.json
├── WebApi.csproj
├── appsettings.Development.json
└── appsettings.json
/.config/dotnet-tools.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "isRoot": true,
4 | "tools": {
5 | "dotnet-validate": {
6 | "version": "0.0.1-preview.537",
7 | "commands": [
8 | "dotnet-validate"
9 | ]
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.sh eol=lf
2 | *.verified.json text eol=lf working-tree-encoding=UTF-8
3 | *.verified.txt text eol=lf working-tree-encoding=UTF-8
4 | *.verified.xml text eol=lf working-tree-encoding=UTF-8
5 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @martincostello
2 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | To contribute changes (source code, scripts, configuration) to this repository please follow the steps below.
4 | These steps are a guideline for contributing and do not necessarily need to be followed for all changes.
5 |
6 | 1. If you intend to fix a bug please create an issue before forking the repository.
7 | 1. Fork the `main` branch of this repository from the latest commit.
8 | 1. Create a branch from your fork's `main` branch to help isolate your changes from any further work on `main`. If fixing an issue try to reference its name in your branch name (e.g. `issue-123`) to make changes easier to track the changes.
9 | 1. Work on your proposed changes on your fork. If you are fixing an issue include at least one unit test that reproduces it if the code changes to fix it have not been applied; if you are adding new functionality please include unit tests appropriate to the changes you are making.
10 | 1. When you think your changes are complete, test that the code builds cleanly using `build.ps1`. There should be no compiler warnings and all tests should pass.
11 | 1. Once your changes build cleanly locally submit a Pull Request back to the `main` branch from your fork's branch. Ideally commits to your branch should be squashed before creating the Pull Request. If the Pull Request fixes an issue please reference it in the title and/or description. Please keep changes focused around a specific topic rather than include multiple types of changes in a single Pull Request.
12 | 1. After your Pull Request is created it will build against the repository's continuous integrations.
13 | 1. Once the Pull Request has been reviewed by the project's [contributors](https://github.com/martincostello/openapi-extensions/graphs/contributors) and the status checks pass your Pull Request will be merged back to the `main` branch, assuming that the changes are deemed appropriate.
14 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [martincostello]
2 | buy_me_a_coffee: martincostello
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: ''
3 | ---
4 |
5 | ### Expected behaviour
6 |
7 |
8 |
9 | ### Actual behaviour
10 |
11 |
12 |
13 | ### Steps to reproduce
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/10_bug_report.yml:
--------------------------------------------------------------------------------
1 | name: 🐛 Bug report
2 | description: Something not behaving as expected?
3 | title: '[Bug]: '
4 | labels: ['bug']
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | Please check for an existing issue and the [README](https://github.com/martincostello/openapi-extensions/blob/main/README.md) before submitting a bug report.
10 |
11 | If you're not using the latest release, please try upgrading to the latest version first to see if the issue resolves itself.
12 | - type: input
13 | attributes:
14 | label: Version
15 | description: Which version of the library are you experiencing the issue with?
16 | placeholder: 1.1.0
17 | validations:
18 | required: true
19 | - type: textarea
20 | attributes:
21 | label: Describe the bug
22 | description: A clear and concise description of what the bug is.
23 | validations:
24 | required: true
25 | - type: textarea
26 | attributes:
27 | label: Expected behaviour
28 | description: A clear and concise description of what you expected to happen.
29 | validations:
30 | required: false
31 | - type: textarea
32 | attributes:
33 | label: Actual behaviour
34 | description: What actually happens.
35 | validations:
36 | required: false
37 | - type: textarea
38 | attributes:
39 | label: Steps to reproduce
40 | description: |
41 | Provide a link to a [minimalistic project which reproduces this issue (repro)](https://stackoverflow.com/help/mcve) hosted in a **public** GitHub repository.
42 | Code snippets, such as a failing unit test or small console app, which demonstrate the issue wrapped in a [fenced code block](https://docs.github.com/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks) are also acceptable.
43 |
44 | This issue will be closed if:
45 | - The behaviour you're reporting cannot be easily reproduced.
46 | - The issue is a duplicate of an existing issue.
47 | - The behaviour you're reporting is by design.
48 | validations:
49 | required: false
50 | - type: textarea
51 | attributes:
52 | label: Exception(s) (if any)
53 | description: Include any exception(s) and/or stack trace(s) you get when facing this issue.
54 | render: text
55 | validations:
56 | required: false
57 | - type: input
58 | attributes:
59 | label: .NET Version
60 | description: |
61 | Run `dotnet --version` to get the .NET SDK version you're using.
62 | Alternatively, which target framework(s) (e.g. `net8.0`) does the project you're using the package with target?
63 | placeholder: 9.0.100
64 | validations:
65 | required: false
66 | - type: textarea
67 | attributes:
68 | label: Anything else?
69 | description: |
70 | Links? References? Anything that will give us more context about the issue you are encountering is useful.
71 |
72 | 💡Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
73 | validations:
74 | required: false
75 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/20_feature_request.yml:
--------------------------------------------------------------------------------
1 | name: 💡 Feature request
2 | description: Suggest a feature request or improvement
3 | title: '[Feature request]: '
4 | labels: ['feature-request']
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | Please check for an existing issue and the [README](https://github.com/martincostello/openapi-extensions/blob/main/README.md) before submitting a feature request.
10 | - type: textarea
11 | attributes:
12 | label: Is your feature request related to a specific problem? Or an existing feature?
13 | description: A clear and concise description of what the problem is. Motivating examples help prioritise things.
14 | placeholder: I am trying to [...] but [...]
15 | validations:
16 | required: true
17 | - type: textarea
18 | attributes:
19 | label: Describe the solution you'd like
20 | description: |
21 | A clear and concise description of what you want to happen. Include any alternative solutions you've considered.
22 | validations:
23 | required: true
24 | - type: textarea
25 | attributes:
26 | label: Additional context
27 | description: |
28 | Add any other context or screenshots about the feature request here.
29 | validations:
30 | required: false
31 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/30_question.yml:
--------------------------------------------------------------------------------
1 | name: 🤔 Question?
2 | description: You have something specific to achieve and the existing documentation hasn't covered how.
3 | title: '[Question]: '
4 | labels: ['question']
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | Please check for an existing issue and the [README](https://github.com/martincostello/openapi-extensions/blob/main/README.md) before asking a question.
10 | - type: textarea
11 | attributes:
12 | label: What do you want to achieve?
13 | description: A clear and concise description of what you're trying to do.
14 | placeholder: I am trying to [...] but [...]
15 | validations:
16 | required: true
17 | - type: textarea
18 | attributes:
19 | label: What code or approach do you have so far?
20 | description: |
21 | Provide a [minimalistic project which shows what you have so far](https://stackoverflow.com/help/mcve) hosted in a **public** GitHub repository.
22 | Code snippets wrapped in a [fenced code block](https://docs.github.com/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks) are also acceptable.
23 | validations:
24 | required: true
25 | - type: textarea
26 | attributes:
27 | label: Additional context
28 | description: |
29 | Add any other context or screenshots related to your question here.
30 | validations:
31 | required: false
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
2 | contact_links:
3 | - name: Contact me
4 | url: https://martincostello.com/bluesky
5 | about: You can also contact me on Bluesky.
6 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/.github/actionlint-matcher.json:
--------------------------------------------------------------------------------
1 | {
2 | "problemMatcher": [
3 | {
4 | "owner": "actionlint",
5 | "pattern": [
6 | {
7 | "regexp": "^(?:\\x1b\\[\\d+m)?(.+?)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*: (?:\\x1b\\[\\d+m)*(.+?)(?:\\x1b\\[\\d+m)* \\[(.+?)\\]$",
8 | "file": 1,
9 | "line": 2,
10 | "column": 3,
11 | "message": 4,
12 | "code": 5
13 | }
14 | ]
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "05:30"
8 | timezone: Europe/London
9 | - package-ecosystem: nuget
10 | directory: "/"
11 | groups:
12 | xunit:
13 | patterns:
14 | - Verify.Xunit*
15 | - xunit*
16 | schedule:
17 | interval: daily
18 | time: "05:30"
19 | timezone: Europe/London
20 | open-pull-requests-limit: 99
21 | ignore:
22 | - dependency-name: "Microsoft.AspNetCore.OpenApi"
23 | - dependency-name: "Microsoft.OpenApi*"
24 |
--------------------------------------------------------------------------------
/.github/update-dotnet-sdk.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/martincostello/github-automation/main/.github/update-dotnet-sdk-schema.json",
3 | "update-nuget-packages": false
4 | }
5 |
--------------------------------------------------------------------------------
/.github/workflows/benchmark.yml:
--------------------------------------------------------------------------------
1 | name: benchmark
2 |
3 | env:
4 | DOTNET_CLI_TELEMETRY_OPTOUT: true
5 | DOTNET_NOLOGO: true
6 | DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION: 1
7 | NUGET_XMLDOC_MODE: skip
8 | TERM: xterm
9 |
10 | on:
11 | push:
12 | branches:
13 | - main
14 | - dotnet-vnext
15 | - dotnet-nightly
16 | paths-ignore:
17 | - '**/*.gitattributes'
18 | - '**/*.gitignore'
19 | - '**/*.md'
20 | workflow_dispatch:
21 |
22 | permissions:
23 | contents: read
24 |
25 | jobs:
26 | benchmark:
27 | name: benchmark
28 | runs-on: ubuntu-latest
29 |
30 | steps:
31 |
32 | - name: Checkout code
33 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
34 | with:
35 | filter: 'tree:0'
36 | show-progress: false
37 |
38 | - name: Setup .NET SDK
39 | uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
40 | with:
41 | dotnet-version: '9.0.x'
42 |
43 | - name: Setup .NET SDK
44 | uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
45 |
46 | - name: Run benchmarks
47 | shell: pwsh
48 | run: ./benchmark.ps1
49 |
50 | - name: Publish BenchmarkDotNet artifacts
51 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
52 | if: ${{ !cancelled() }}
53 | with:
54 | name: artifacts
55 | path: ./BenchmarkDotNet.Artifacts/results/*
56 | if-no-files-found: error
57 |
58 | - name: Get repository name
59 | id: get-repo-name
60 | shell: pwsh
61 | run: |
62 | $repoName = ${env:GITHUB_REPOSITORY}.Split("/")[-1]
63 | "repo-name=${repoName}" >> ${env:GITHUB_OUTPUT}
64 |
65 | - name: Publish results
66 | uses: martincostello/benchmarkdotnet-results-publisher@abcb3ce3975e1e86f06f2c04e3a4059ccdb91cc1 # v1.0.2
67 | with:
68 | branch: ${{ github.ref_name }}
69 | comment-on-threshold: true
70 | name: 'OpenAPI Extensions'
71 | output-file-path: '${{ steps.get-repo-name.outputs.repo-name }}/data.json'
72 | repo: '${{ github.repository_owner }}/benchmarks'
73 | repo-token: ${{ secrets.BENCHMARKS_TOKEN }}
74 |
75 | - name: Output summary
76 | shell: pwsh
77 | env:
78 | REPO_NAME: ${{ steps.get-repo-name.outputs.repo-name }}
79 | run: |
80 | $summary += "`n`n"
81 | $summary += "View benchmark results history [here](https://benchmarks.martincostello.com/?repo=${env:REPO_NAME}&branch=${env:GITHUB_REF_NAME})."
82 | $summary >> ${env:GITHUB_STEP_SUMMARY}
83 |
--------------------------------------------------------------------------------
/.github/workflows/code-scan.yml:
--------------------------------------------------------------------------------
1 | name: code-scan
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches:
8 | - main
9 | - dotnet-vnext
10 | - dotnet-nightly
11 | schedule:
12 | - cron: '0 6 * * MON'
13 | workflow_dispatch:
14 |
15 | permissions:
16 | actions: read
17 | contents: read
18 |
19 | jobs:
20 | code-ql:
21 |
22 | runs-on: ubuntu-latest
23 |
24 | permissions:
25 | security-events: write
26 |
27 | steps:
28 | - name: Checkout repository
29 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
30 | with:
31 | filter: 'tree:0'
32 | show-progress: false
33 |
34 | - name: Initialize CodeQL
35 | uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
36 | with:
37 | build-mode: none
38 | languages: 'csharp'
39 | queries: security-and-quality
40 |
41 | - name: Perform CodeQL Analysis
42 | uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
43 | with:
44 | category: '/language:csharp'
45 |
--------------------------------------------------------------------------------
/.github/workflows/dependency-review.yml:
--------------------------------------------------------------------------------
1 | name: dependency-review
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | - dotnet-vnext
8 | - dotnet-nightly
9 |
10 | permissions:
11 | contents: read
12 |
13 | jobs:
14 | dependency-review:
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 |
19 | - name: Checkout code
20 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
21 | with:
22 | filter: 'tree:0'
23 | show-progress: false
24 |
25 | - name: Review dependencies
26 | uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1
27 | with:
28 | allow-licenses: 'Apache-2.0,BSD-3-Clause,MIT'
29 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: lint
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | paths-ignore:
7 | - '**/*.gitattributes'
8 | - '**/*.gitignore'
9 | - '**/*.md'
10 | pull_request:
11 | branches:
12 | - main
13 | - dotnet-vnext
14 | - dotnet-nightly
15 | workflow_dispatch:
16 |
17 | permissions:
18 | contents: read
19 |
20 | env:
21 | FORCE_COLOR: 3
22 | POWERSHELL_YAML_VERSION: '0.4.12'
23 | PSSCRIPTANALYZER_VERSION: '1.24.0'
24 | TERM: xterm
25 |
26 | jobs:
27 | lint:
28 | runs-on: ubuntu-latest
29 |
30 | steps:
31 |
32 | - name: Checkout code
33 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
34 | with:
35 | filter: 'tree:0'
36 | show-progress: false
37 |
38 | - name: Add actionlint problem matcher
39 | run: echo "::add-matcher::.github/actionlint-matcher.json"
40 |
41 | - name: Lint workflows
42 | uses: docker://rhysd/actionlint@sha256:887a259a5a534f3c4f36cb02dca341673c6089431057242cdc931e9f133147e9 # v1.7.7
43 | with:
44 | args: -color
45 |
46 | - name: Lint markdown
47 | uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e # v20.0.0
48 | with:
49 | config: '.markdownlint.json'
50 | globs: |
51 | **/*.md
52 |
53 | - name: Lint PowerShell in workflows
54 | uses: martincostello/lint-actions-powershell@5942e3350ee5bd8f8933cec4e1185d13f0ea688f # v1.0.0
55 | with:
56 | powershell-yaml-version: ${{ env.POWERSHELL_YAML_VERSION }}
57 | psscriptanalyzer-version: ${{ env.PSSCRIPTANALYZER_VERSION }}
58 | treat-warnings-as-errors: true
59 |
60 | - name: Lint PowerShell scripts
61 | shell: pwsh
62 | run: |
63 | $settings = @{
64 | IncludeDefaultRules = $true
65 | Severity = @("Error", "Warning")
66 | }
67 | $issues = Invoke-ScriptAnalyzer -Path ${env:GITHUB_WORKSPACE} -Recurse -ReportSummary -Settings $settings
68 | foreach ($issue in $issues) {
69 | $severity = $issue.Severity.ToString()
70 | $level = $severity.Contains("Error") ? "error" : $severity.Contains("Warning") ? "warning" : "notice"
71 | Write-Output "::${level} file=$($issue.ScriptName),line=$($issue.Line),title=PSScriptAnalyzer::$($issue.Message)"
72 | }
73 | if ($issues.Count -gt 0) {
74 | exit 1
75 | }
76 |
--------------------------------------------------------------------------------
/.github/workflows/ossf-scorecard.yml:
--------------------------------------------------------------------------------
1 | name: ossf-scorecard
2 |
3 | on:
4 | branch_protection_rule:
5 | push:
6 | branches: [ main ]
7 | schedule:
8 | - cron: '0 5 * * MON'
9 | workflow_dispatch:
10 |
11 | permissions: read-all
12 |
13 | jobs:
14 | analysis:
15 | name: analysis
16 | runs-on: ubuntu-latest
17 |
18 | permissions:
19 | id-token: write
20 | security-events: write
21 |
22 | steps:
23 | - name: Checkout code
24 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
25 | with:
26 | filter: 'tree:0'
27 | persist-credentials: false
28 | show-progress: false
29 |
30 | - name: Run analysis
31 | uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
32 | with:
33 | publish_results: true
34 | results_file: results.sarif
35 | results_format: sarif
36 |
37 | - name: Upload artifact
38 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
39 | with:
40 | name: SARIF
41 | path: results.sarif
42 | retention-days: 5
43 |
44 | - name: Upload to code-scanning
45 | uses: github/codeql-action/upload-sarif@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
46 | with:
47 | sarif_file: results.sarif
48 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | publish:
7 | description: 'If true, does not create the release as a draft.'
8 | required: false
9 | type: boolean
10 | default: false
11 |
12 | permissions: {}
13 |
14 | jobs:
15 | release:
16 | runs-on: [ ubuntu-latest ]
17 |
18 | concurrency:
19 | group: ${{ github.workflow }}
20 | cancel-in-progress: false
21 |
22 | steps:
23 |
24 | - name: Checkout code
25 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
26 | with:
27 | filter: 'tree:0'
28 | show-progress: false
29 | token: ${{ secrets.COSTELLOBOT_TOKEN }}
30 |
31 | - name: Get version
32 | id: get-version
33 | shell: pwsh
34 | run: |
35 | $properties = Join-Path "." "Directory.Build.props"
36 | $xml = [xml](Get-Content $properties)
37 | $version = $xml.SelectSingleNode('Project/PropertyGroup/VersionPrefix').InnerText
38 | "version=${version}" >> $env:GITHUB_OUTPUT
39 |
40 | - name: Create release
41 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
42 | env:
43 | DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
44 | DRAFT: ${{ inputs.publish != true }}
45 | VERSION: ${{ steps.get-version.outputs.version }}
46 | with:
47 | github-token: ${{ secrets.COSTELLOBOT_TOKEN }}
48 | script: |
49 | const { repo, owner } = context.repo;
50 | const draft = process.env.DRAFT === 'true';
51 | const version = process.env.VERSION;
52 | const tag_name = `v${version}`;
53 | const name = tag_name;
54 |
55 | const { data: notes } = await github.rest.repos.generateReleaseNotes({
56 | owner,
57 | repo,
58 | tag_name,
59 | target_commitish: process.env.DEFAULT_BRANCH,
60 | });
61 |
62 | const body = notes.body
63 | .split('\n')
64 | .filter((line) => !line.includes(' @costellobot '))
65 | .filter((line) => !line.includes(' @dependabot '))
66 | .filter((line) => !line.includes(' @github-actions '))
67 | .join('\n');
68 |
69 | const { data: release } = await github.rest.repos.createRelease({
70 | owner,
71 | repo,
72 | tag_name,
73 | name,
74 | body,
75 | draft,
76 | });
77 |
78 | core.notice(`Created release ${release.name}: ${release.html_url}`);
79 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .dotnet
3 | .idea
4 | .metadata
5 | .settings
6 | .vs
7 | _ReSharper*
8 | _reports
9 | _UpgradeReport_Files/
10 | artifacts/
11 | Backup*/
12 | BenchmarkDotNet.Artifacts/
13 | bin
14 | Bin
15 | coverage
16 | coverage.*
17 | junit.xml
18 | MSBuild_Logs/
19 | node_modules
20 | obj
21 | packages
22 | project.lock.json
23 | TestResults
24 | typings
25 | UpgradeLog*.htm
26 | UpgradeLog*.XML
27 | PublishProfiles
28 | *.binlog
29 | *.db
30 | *.db-shm
31 | *.db-wal
32 | *.DotSettings
33 | *.GhostDoc.xml
34 | *.log
35 | *.nupkg
36 | !.packages/*.nupkg
37 | *.opensdf
38 | *.[Pp]ublish.xml
39 | *.publishproj
40 | *.pubxml
41 | *.received.*
42 | *.sdf
43 | *.sln.cache
44 | *.sln.docstates
45 | *.sln.ide
46 | *.suo
47 | *.user
48 |
--------------------------------------------------------------------------------
/.markdownlint.json:
--------------------------------------------------------------------------------
1 | {
2 | "MD013": false,
3 | "MD040": false
4 | }
5 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "EditorConfig.EditorConfig",
4 | "ms-dotnettools.csharp",
5 | "ms-vscode.PowerShell"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Run tests",
6 | "type": "coreclr",
7 | "request": "launch",
8 | "preLaunchTask": "build",
9 | "program": "dotnet",
10 | "args": [
11 | "test"
12 | ],
13 | "cwd": "${workspaceFolder}/tests/OpenApi.Extensions.Tests",
14 | "console": "internalConsole",
15 | "stopAtEntry": false,
16 | "internalConsoleOptions": "openOnSessionStart"
17 | }
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "terminal.integrated.defaultProfile.linux": "pwsh",
3 | "terminal.integrated.defaultProfile.osx": "pwsh",
4 | "terminal.integrated.defaultProfile.windows": "PowerShell"
5 | }
6 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "build",
6 | "command": "dotnet build",
7 | "type": "shell",
8 | "group": "build",
9 | "presentation": {
10 | "reveal": "silent"
11 | },
12 | "problemMatcher": "$msCompile"
13 | }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/.vsconfig:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0",
3 | "components": [
4 | "Microsoft.NetCore.Component.Runtime.9.0",
5 | "Microsoft.NetCore.Component.SDK",
6 | "Microsoft.VisualStudio.Component.CoreEditor",
7 | "Microsoft.VisualStudio.Component.Roslyn.Compiler",
8 | "Microsoft.VisualStudio.Component.Roslyn.LanguageServices",
9 | "Microsoft.VisualStudio.Workload.CoreEditor"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team [through a GitHub issue][issue].
59 | All complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [https://contributor-covenant.org/version/1/4][version]
72 |
73 | [issue]: https://github.com/martincostello/openapi-extensions/issues
74 | [homepage]: https://contributor-covenant.org
75 | [version]: https://contributor-covenant.org/version/1/4/
76 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | false
4 | $(MSBuildThisFileDirectory)OpenApi.Extensions.ruleset
5 | true
6 | openapi
7 | true
8 | true
9 | 1.1.0
10 | 1.1.1
11 |
12 |
13 | true
14 | $(NoWarn);419;1570;1573;1574;1584;1591;SA0001;SA1602
15 |
16 |
17 |
--------------------------------------------------------------------------------
/Directory.Packages.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/NuGet.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/OpenApi.Extensions.ruleset:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/OpenApi.Extensions.slnx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/SECURITY.MD:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Reporting a Vulnerability
4 |
5 | To privately report a security vulnerability, please create a security advisory in the [repository's Security tab][advisories].
6 |
7 | Further details can be found in the [GitHub documentation][reporting].
8 |
9 | [advisories]: https://github.com/martincostello/openapi-extensions/security/advisories
10 | [reporting]: https://docs.github.com/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability
11 |
--------------------------------------------------------------------------------
/benchmark.ps1:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env pwsh
2 |
3 | #Requires -PSEdition Core
4 | #Requires -Version 7
5 |
6 | param(
7 | [Parameter(Mandatory = $false)][string] $Filter = "",
8 | [Parameter(Mandatory = $false)][string] $Job = ""
9 | )
10 |
11 | $ErrorActionPreference = "Stop"
12 | $ProgressPreference = "SilentlyContinue"
13 |
14 | $solutionPath = $PSScriptRoot
15 | $sdkFile = Join-Path $solutionPath "global.json"
16 |
17 | $dotnetVersion = (Get-Content $sdkFile | Out-String | ConvertFrom-Json).sdk.version
18 |
19 | $installDotNetSdk = $false
20 |
21 | if (($null -eq (Get-Command "dotnet" -ErrorAction SilentlyContinue)) -and ($null -eq (Get-Command "dotnet.exe" -ErrorAction SilentlyContinue))) {
22 | Write-Output "The .NET SDK is not installed."
23 | $installDotNetSdk = $true
24 | }
25 | else {
26 | Try {
27 | $installedDotNetVersion = (dotnet --version 2>&1 | Out-String).Trim()
28 | }
29 | Catch {
30 | $installedDotNetVersion = "?"
31 | }
32 |
33 | if ($installedDotNetVersion -ne $dotnetVersion) {
34 | Write-Output "The required version of the .NET SDK is not installed. Expected $dotnetVersion."
35 | $installDotNetSdk = $true
36 | }
37 | }
38 |
39 | if ($installDotNetSdk) {
40 | ${env:DOTNET_INSTALL_DIR} = Join-Path $PSScriptRoot ".dotnet"
41 | $sdkPath = Join-Path ${env:DOTNET_INSTALL_DIR} "sdk" $dotnetVersion
42 |
43 | if (-Not (Test-Path $sdkPath)) {
44 | if (-Not (Test-Path ${env:DOTNET_INSTALL_DIR})) {
45 | mkdir ${env:DOTNET_INSTALL_DIR} | Out-Null
46 | }
47 | [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor "Tls12"
48 |
49 | if (($PSVersionTable.PSVersion.Major -ge 6) -And (-Not $IsWindows)) {
50 | $installScript = Join-Path ${env:DOTNET_INSTALL_DIR} "install.sh"
51 | Invoke-WebRequest "https://dot.net/v1/dotnet-install.sh" -OutFile $installScript -UseBasicParsing
52 | chmod +x $installScript
53 | & $installScript --jsonfile $sdkFile --install-dir ${env:DOTNET_INSTALL_DIR} --no-path
54 | }
55 | else {
56 | $installScript = Join-Path ${env:DOTNET_INSTALL_DIR} "install.ps1"
57 | Invoke-WebRequest "https://dot.net/v1/dotnet-install.ps1" -OutFile $installScript -UseBasicParsing
58 | & $installScript -JsonFile $sdkFile -InstallDir ${env:DOTNET_INSTALL_DIR} -NoPath
59 | }
60 | }
61 | }
62 | else {
63 | ${env:DOTNET_INSTALL_DIR} = Split-Path -Path (Get-Command dotnet).Path
64 | }
65 |
66 | $dotnet = Join-Path ${env:DOTNET_INSTALL_DIR} "dotnet"
67 |
68 | if ($installDotNetSdk) {
69 | ${env:PATH} = "${env:DOTNET_INSTALL_DIR};${env:PATH}"
70 | }
71 |
72 | $benchmarks = (Join-Path $solutionPath "perf" "OpenApi.Extensions.Benchmarks" "MartinCostello.OpenApi.Extensions.Benchmarks.csproj")
73 |
74 | Write-Output "Running benchmarks..."
75 |
76 | $additionalArgs = @()
77 |
78 | if (-Not [string]::IsNullOrEmpty($Filter)) {
79 | $additionalArgs += "--filter"
80 | $additionalArgs += $Filter
81 | }
82 |
83 | if (-Not [string]::IsNullOrEmpty($Job)) {
84 | $additionalArgs += "--job"
85 | $additionalArgs += $Job
86 | }
87 |
88 | if (-Not [string]::IsNullOrEmpty(${env:GITHUB_SHA})) {
89 | $additionalArgs += "--exporters"
90 | $additionalArgs += "json"
91 | }
92 |
93 | & $dotnet run --project $benchmarks --configuration "Release" -- $additionalArgs
94 |
--------------------------------------------------------------------------------
/build.ps1:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env pwsh
2 |
3 | #Requires -PSEdition Core
4 | #Requires -Version 7
5 |
6 | param(
7 | [Parameter(Mandatory = $false)][switch] $SkipTests
8 | )
9 |
10 | $ErrorActionPreference = "Stop"
11 | $ProgressPreference = "SilentlyContinue"
12 |
13 | $solutionPath = $PSScriptRoot
14 | $sdkFile = Join-Path $solutionPath "global.json"
15 |
16 | $dotnetVersion = (Get-Content $sdkFile | Out-String | ConvertFrom-Json).sdk.version
17 |
18 | $installDotNetSdk = $false
19 |
20 | if (($null -eq (Get-Command "dotnet" -ErrorAction SilentlyContinue)) -and ($null -eq (Get-Command "dotnet.exe" -ErrorAction SilentlyContinue))) {
21 | Write-Output "The .NET SDK is not installed."
22 | $installDotNetSdk = $true
23 | }
24 | else {
25 | Try {
26 | $installedDotNetVersion = (dotnet --version 2>&1 | Out-String).Trim()
27 | }
28 | Catch {
29 | $installedDotNetVersion = "?"
30 | }
31 |
32 | if ($installedDotNetVersion -ne $dotnetVersion) {
33 | Write-Output "The required version of the .NET SDK is not installed. Expected $dotnetVersion."
34 | $installDotNetSdk = $true
35 | }
36 | }
37 |
38 | if ($installDotNetSdk) {
39 |
40 | ${env:DOTNET_INSTALL_DIR} = Join-Path $PSScriptRoot ".dotnet"
41 | $sdkPath = Join-Path ${env:DOTNET_INSTALL_DIR} "sdk" $dotnetVersion
42 |
43 | if (-Not (Test-Path $sdkPath)) {
44 | if (-Not (Test-Path ${env:DOTNET_INSTALL_DIR})) {
45 | mkdir ${env:DOTNET_INSTALL_DIR} | Out-Null
46 | }
47 | [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor "Tls12"
48 | if (($PSVersionTable.PSVersion.Major -ge 6) -And (-Not $IsWindows)) {
49 | $installScript = Join-Path ${env:DOTNET_INSTALL_DIR} "install.sh"
50 | Invoke-WebRequest "https://dot.net/v1/dotnet-install.sh" -OutFile $installScript -UseBasicParsing
51 | chmod +x $installScript
52 | & $installScript --jsonfile $sdkFile --install-dir ${env:DOTNET_INSTALL_DIR} --no-path --skip-non-versioned-files
53 | }
54 | else {
55 | $installScript = Join-Path ${env:DOTNET_INSTALL_DIR} "install.ps1"
56 | Invoke-WebRequest "https://dot.net/v1/dotnet-install.ps1" -OutFile $installScript -UseBasicParsing
57 | & $installScript -JsonFile $sdkFile -InstallDir ${env:DOTNET_INSTALL_DIR} -NoPath -SkipNonVersionedFiles
58 | }
59 | }
60 | }
61 | else {
62 | ${env:DOTNET_INSTALL_DIR} = Split-Path -Path (Get-Command dotnet).Path
63 | }
64 |
65 | $dotnet = Join-Path ${env:DOTNET_INSTALL_DIR} "dotnet"
66 |
67 | if ($installDotNetSdk) {
68 | ${env:PATH} = "${env:DOTNET_INSTALL_DIR};${env:PATH}"
69 | }
70 |
71 | function DotNetPack {
72 | param([string]$Project)
73 |
74 | & $dotnet pack $Project --include-symbols --include-source
75 |
76 | if ($LASTEXITCODE -ne 0) {
77 | throw "dotnet pack failed with exit code $LASTEXITCODE"
78 | }
79 | }
80 |
81 | function DotNetTest {
82 | param()
83 |
84 | $additionalArgs = @()
85 |
86 | if (-Not [string]::IsNullOrEmpty(${env:GITHUB_SHA})) {
87 | $additionalArgs += "--logger:GitHubActions;report-warnings=false"
88 | $additionalArgs += "--logger:junit;LogFilePath=junit.xml"
89 | }
90 |
91 | & $dotnet test --configuration "Release" $additionalArgs
92 |
93 | if ($LASTEXITCODE -ne 0) {
94 | throw "dotnet test failed with exit code $LASTEXITCODE"
95 | }
96 | }
97 |
98 | $packageProjects = @(
99 | (Join-Path $solutionPath "src" "OpenApi.Extensions" "MartinCostello.OpenApi.Extensions.csproj")
100 | )
101 |
102 | Write-Output "Packaging libraries..."
103 | ForEach ($project in $packageProjects) {
104 | DotNetPack $project $Configuration
105 | }
106 |
107 | if (-Not $SkipTests) {
108 | Write-Output "Testing solution..."
109 | DotNetTest
110 | }
111 |
--------------------------------------------------------------------------------
/exclusion.dic:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "version": "9.0.301",
4 | "allowPrerelease": false,
5 | "rollForward": "latestMajor",
6 | "paths": [ ".dotnet", "$host$" ],
7 | "errorMessage": "The required version of the .NET SDK could not be found. Please run ./build.ps1 to bootstrap the .NET SDK."
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/omnisharp.json:
--------------------------------------------------------------------------------
1 | {
2 | "fileOptions": {
3 | "systemExcludeSearchPatterns": [
4 | "**/bin/**/*",
5 | "**/obj/**/*",
6 | "**/node_modules/**/*"
7 | ],
8 | "excludeSearchPatterns": []
9 | },
10 | "FormattingOptions": {
11 | "enableEditorConfigSupport": true
12 | },
13 | "msbuild": {
14 | "MSBuildSDKsPath": ".\\.dotnet",
15 | "EnablePackageAutoRestore": true,
16 | "loadProjectsOnDemand": true
17 | },
18 | "RoslynExtensionsOptions": {
19 | "enableAnalyzersSupport": true
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/package-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martincostello/openapi-extensions/ce1cc678ff86186a2ad58f910f3e183796449a21/package-icon.png
--------------------------------------------------------------------------------
/package-readme.md:
--------------------------------------------------------------------------------
1 | # OpenAPI Extensions
2 |
3 | [![NuGet][package-badge-version]][package-download]
4 | [![NuGet Downloads][package-badge-downloads]][package-download]
5 | [![Build status][build-badge]][build-status]
6 |
7 | ## Introduction
8 |
9 | Extensions for the [Microsoft.AspNetCore.OpenApi][aspnetcore-openapi] library.
10 |
11 | ## Feedback
12 |
13 | Any feedback or issues for this package can be added to the issues in [GitHub][issues].
14 |
15 | ## License
16 |
17 | This package is licensed under the [Apache 2.0][license] license.
18 |
19 | [aspnetcore-openapi]: https://www.nuget.org/packages/Microsoft.AspNetCore.OpenApi
20 | [build-badge]: https://github.com/martincostello/openapi-extensions/actions/workflows/build.yml/badge.svg?branch=main&event=push
21 | [build-status]: https://github.com/martincostello/openapi-extensions/actions?query=workflow%3Abuild+branch%3Amain+event%3Apush "Continuous Integration for this project"
22 | [issues]: https://github.com/martincostello/openapi-extensions/issues "Issues for this project on GitHub.com"
23 | [license]: https://www.apache.org/licenses/LICENSE-2.0.txt "The Apache 2.0 license"
24 | [package-badge-downloads]: https://img.shields.io/nuget/dt/MartinCostello.OpenApi.Extensions?logo=nuget&label=Downloads&color=blue
25 | [package-badge-version]: https://img.shields.io/nuget/v/MartinCostello.OpenApi.Extensions?logo=nuget&label=Latest&color=blue
26 | [package-download]: https://www.nuget.org/packages/MartinCostello.OpenApi.Extensions "Download MartinCostello.OpenApi.Extensions from NuGet"
27 |
--------------------------------------------------------------------------------
/perf/OpenApi.Extensions.Benchmarks/MartinCostello.OpenApi.Extensions.Benchmarks.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | $(NoWarn);SA1600
4 | Exe
5 | MartinCostello.OpenApi
6 | net9.0
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/perf/OpenApi.Extensions.Benchmarks/OpenApiBenchmarks.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using BenchmarkDotNet.Attributes;
5 | using BenchmarkDotNet.Diagnosers;
6 |
7 | namespace MartinCostello.OpenApi;
8 |
9 | [EventPipeProfiler(EventPipeProfile.CpuSampling)]
10 | [MemoryDiagnoser]
11 | public class OpenApiBenchmarks : IAsyncDisposable
12 | {
13 | private TodoAppServer? _app = new();
14 | private HttpClient? _client;
15 | private bool _disposed;
16 |
17 | [GlobalSetup]
18 | public async Task StartServer()
19 | {
20 | if (_app is { } app)
21 | {
22 | await app.StartAsync();
23 | _client = app.CreateHttpClient();
24 | }
25 | }
26 |
27 | [GlobalCleanup]
28 | public async Task StopServer()
29 | {
30 | if (_app is { } app)
31 | {
32 | await app.StopAsync();
33 | _app = null;
34 | }
35 | }
36 |
37 | [Benchmark(Baseline = true)]
38 | public async Task GetOpenApiDocumentJson()
39 | => await _client!.GetStringAsync("/openapi/v1.json");
40 |
41 | [Benchmark]
42 | public async Task GetOpenApiDocumentYaml()
43 | => await _client!.GetStringAsync("/openapi/v1.yaml");
44 |
45 | public async ValueTask DisposeAsync()
46 | {
47 | GC.SuppressFinalize(this);
48 |
49 | if (!_disposed)
50 | {
51 | _client?.Dispose();
52 | _client = null;
53 |
54 | if (_app is not null)
55 | {
56 | await _app.DisposeAsync();
57 | _app = null;
58 | }
59 | }
60 |
61 | _disposed = true;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/perf/OpenApi.Extensions.Benchmarks/Program.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using BenchmarkDotNet.Running;
5 | using MartinCostello.OpenApi;
6 |
7 | if (args.SequenceEqual(["--test"]))
8 | {
9 | await using var benchmarks = new OpenApiBenchmarks();
10 | await benchmarks.StartServer();
11 |
12 | try
13 | {
14 | _ = await benchmarks.GetOpenApiDocumentJson();
15 | _ = await benchmarks.GetOpenApiDocumentYaml();
16 | }
17 | finally
18 | {
19 | await benchmarks.StopServer();
20 | }
21 | }
22 | else
23 | {
24 | BenchmarkRunner.Run(args: args);
25 | }
26 |
--------------------------------------------------------------------------------
/perf/OpenApi.Extensions.Benchmarks/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "MartinCostello.OpenApi.Extensions.Benchmarks": {
4 | "commandName": "Project",
5 | "commandLineArgs": ""
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/perf/OpenApi.Extensions.Benchmarks/TodoAppServer.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using Microsoft.AspNetCore.Builder;
5 | using Microsoft.AspNetCore.Hosting;
6 | using Microsoft.AspNetCore.Hosting.Server;
7 | using Microsoft.AspNetCore.Hosting.Server.Features;
8 | using Microsoft.Extensions.DependencyInjection;
9 | using Microsoft.Extensions.Logging;
10 | using TodoApp;
11 |
12 | namespace MartinCostello.OpenApi;
13 |
14 | internal sealed class TodoAppServer : IAsyncDisposable
15 | {
16 | private WebApplication? _app;
17 | private Uri? _baseAddress;
18 | private bool _disposed;
19 |
20 | public TodoAppServer()
21 | {
22 | var builder = WebApplication.CreateBuilder([$"--contentRoot={GetContentRoot()}"]);
23 |
24 | builder.Logging.ClearProviders();
25 | builder.WebHost.UseUrls("https://127.0.0.1:0");
26 |
27 | builder.AddTodoApp();
28 |
29 | _app = builder.Build();
30 | _app.UseTodoApp();
31 | }
32 |
33 | public HttpClient CreateHttpClient()
34 | {
35 | var handler = new HttpClientHandler
36 | {
37 | ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator,
38 | };
39 |
40 | #pragma warning disable CA5400
41 | return new(handler, disposeHandler: true) { BaseAddress = _baseAddress };
42 | #pragma warning restore CA5400
43 | }
44 |
45 | public async Task StartAsync()
46 | {
47 | if (_app is { } app)
48 | {
49 | await app.StartAsync();
50 |
51 | var server = app.Services.GetRequiredService();
52 | var addresses = server.Features.Get();
53 |
54 | _baseAddress = addresses!.Addresses
55 | .Select((p) => new Uri(p))
56 | .Last();
57 | }
58 | }
59 |
60 | public async Task StopAsync()
61 | {
62 | if (_app is { } app)
63 | {
64 | await app.StopAsync();
65 | _app = null;
66 | }
67 | }
68 |
69 | public async ValueTask DisposeAsync()
70 | {
71 | GC.SuppressFinalize(this);
72 |
73 | if (!_disposed && _app is not null)
74 | {
75 | await _app.DisposeAsync();
76 | }
77 |
78 | _disposed = true;
79 | }
80 |
81 | private static string GetContentRoot()
82 | {
83 | string contentRoot = string.Empty;
84 | var directoryInfo = new DirectoryInfo(Path.GetDirectoryName(typeof(OpenApiBenchmarks).Assembly.Location)!);
85 |
86 | do
87 | {
88 | string? solutionPath = Directory.EnumerateFiles(directoryInfo.FullName, "TodoApp.slnx").FirstOrDefault();
89 |
90 | if (solutionPath is not null)
91 | {
92 | contentRoot = Path.GetFullPath(Path.Combine(directoryInfo.FullName, "samples", "TodoApp"));
93 | break;
94 | }
95 |
96 | directoryInfo = directoryInfo.Parent;
97 | }
98 | while (directoryInfo is not null);
99 |
100 | return contentRoot;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/samples/TodoApp/Data/ITodoRepository.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace TodoApp.Data;
5 |
6 | ///
7 | /// Represents a repository of Todo items.
8 | ///
9 | public interface ITodoRepository
10 | {
11 | ///
12 | /// Adds a new Todo item.
13 | ///
14 | /// The text of the Todo item.
15 | /// The to use.
16 | ///
17 | /// A representing the asynchronous operation that returns the ID of the new Todo item.
18 | ///
19 | Task AddItemAsync(string text, CancellationToken cancellationToken = default);
20 |
21 | ///
22 | /// Marks a Todo item as completed.
23 | ///
24 | /// The ID of the Todo item to complete.
25 | /// The to use.
26 | ///
27 | /// A representing the asynchronous operation that returns a value indicating whether the Todo item was completed.
28 | ///
29 | Task CompleteItemAsync(Guid itemId, CancellationToken cancellationToken = default);
30 |
31 | ///
32 | /// Deletes a Todo item.
33 | ///
34 | /// The ID of the Todo item to delete.
35 | /// The to use.
36 | ///
37 | /// A representing the asynchronous operation that returns a value indicating whether the Todo item was deleted.
38 | ///
39 | Task DeleteItemAsync(Guid itemId, CancellationToken cancellationToken = default);
40 |
41 | ///
42 | /// Gets a Todo item by its ID.
43 | ///
44 | /// The ID of the Todo item.
45 | /// The to use.
46 | ///
47 | /// A representing the asynchronous operation that returns the Todo item, if found; otherwise .
48 | ///
49 | Task GetItemAsync(Guid itemId, CancellationToken cancellationToken = default);
50 |
51 | ///
52 | /// Gets a list of Todo items.
53 | ///
54 | /// The to use.
55 | ///
56 | /// A representing the asynchronous operation that returns the list of Todo items.
57 | ///
58 | Task> GetItemsAsync(CancellationToken cancellationToken = default);
59 |
60 | ///
61 | /// Gets a list of Todo items that match the specified criteria.
62 | ///
63 | /// The prefix to search by.
64 | /// Whether to search completed items.
65 | /// The to use.
66 | ///
67 | /// A representing the asynchronous operation that returns the list of matching Todo items.
68 | ///
69 | Task> FindAsync(string prefix, bool isCompleted, CancellationToken cancellationToken = default);
70 |
71 | ///
72 | /// Gets a list of Todo items created after the specified date and time.
73 | ///
74 | /// The date and time to look for items created after.
75 | /// The to use.
76 | ///
77 | /// A representing the asynchronous operation that returns the list of matching Todo items.
78 | ///
79 | Task> GetAfterDateAsync(DateTime value, CancellationToken cancellationToken);
80 | }
81 |
--------------------------------------------------------------------------------
/samples/TodoApp/Data/TodoContext.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using Microsoft.EntityFrameworkCore;
5 |
6 | namespace TodoApp.Data;
7 |
8 | ///
9 | /// Represents the data context for the Todo application.
10 | ///
11 | /// The data context options to use.
12 | public class TodoContext(DbContextOptions options) : DbContext(options)
13 | {
14 | ///
15 | /// Gets or sets the Todo items.
16 | ///
17 | public DbSet Items { get; set; } = default!;
18 | }
19 |
--------------------------------------------------------------------------------
/samples/TodoApp/Data/TodoItem.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace TodoApp.Data;
5 |
6 | ///
7 | /// A class representing a Todo item.
8 | ///
9 | public class TodoItem
10 | {
11 | ///
12 | /// Gets or sets the ID of the Todo item.
13 | ///
14 | public Guid Id { get; set; }
15 |
16 | ///
17 | /// Gets or sets the text of the Todo item.
18 | ///
19 | public string Text { get; set; } = default!;
20 |
21 | ///
22 | /// Gets or sets the date and time the Todo item was created.
23 | ///
24 | public DateTime CreatedAt { get; set; }
25 |
26 | ///
27 | /// Gets or sets the date and time the Todo item was completed, if any.
28 | ///
29 | public DateTime? CompletedAt { get; set; }
30 | }
31 |
--------------------------------------------------------------------------------
/samples/TodoApp/Data/TodoRepository.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using Microsoft.EntityFrameworkCore;
5 |
6 | namespace TodoApp.Data;
7 |
8 | ///
9 | /// Represents a repository of Todo items.
10 | ///
11 | /// The to use.
12 | /// The to use.
13 | public class TodoRepository(TimeProvider timeProvider, TodoContext context) : ITodoRepository
14 | {
15 | ///
16 | public async Task AddItemAsync(string text, CancellationToken cancellationToken = default)
17 | {
18 | await EnsureDatabaseAsync(cancellationToken);
19 |
20 | var item = new TodoItem
21 | {
22 | CreatedAt = UtcNow(),
23 | Text = text,
24 | };
25 |
26 | context.Add(item);
27 |
28 | await context.SaveChangesAsync(cancellationToken);
29 |
30 | return item;
31 | }
32 |
33 | ///
34 | public async Task CompleteItemAsync(Guid itemId, CancellationToken cancellationToken = default)
35 | {
36 | var item = await GetItemAsync(itemId, cancellationToken);
37 |
38 | if (item is null)
39 | {
40 | return null;
41 | }
42 |
43 | if (item.CompletedAt.HasValue)
44 | {
45 | return false;
46 | }
47 |
48 | item.CompletedAt = UtcNow();
49 |
50 | context.Items.Update(item);
51 |
52 | await context.SaveChangesAsync(cancellationToken);
53 |
54 | return true;
55 | }
56 |
57 | ///
58 | public async Task DeleteItemAsync(Guid itemId, CancellationToken cancellationToken = default)
59 | {
60 | var item = await GetItemAsync(itemId, cancellationToken);
61 |
62 | if (item is null)
63 | {
64 | return false;
65 | }
66 |
67 | context.Items.Remove(item);
68 |
69 | await context.SaveChangesAsync(cancellationToken);
70 |
71 | return true;
72 | }
73 |
74 | ///
75 | public async Task GetItemAsync(Guid itemId, CancellationToken cancellationToken = default)
76 | {
77 | await EnsureDatabaseAsync(cancellationToken);
78 |
79 | return await context.Items.FindItemAsync(itemId, cancellationToken);
80 | }
81 |
82 | ///
83 | public async Task> GetItemsAsync(CancellationToken cancellationToken = default)
84 | {
85 | await EnsureDatabaseAsync(cancellationToken);
86 |
87 | return await context.Items
88 | .OrderBy(x => x.CompletedAt.HasValue)
89 | .ThenBy(x => x.CreatedAt)
90 | .ToListAsync(cancellationToken);
91 | }
92 |
93 | ///
94 | public async Task> FindAsync(
95 | string prefix,
96 | bool isCompleted,
97 | CancellationToken cancellationToken = default)
98 | {
99 | await EnsureDatabaseAsync(cancellationToken);
100 |
101 | return await context.Items
102 | .Where(x => x.Text.StartsWith(prefix))
103 | .Where(x => x.CompletedAt.HasValue == isCompleted)
104 | .ToListAsync(cancellationToken);
105 | }
106 |
107 | ///
108 | public async Task> GetAfterDateAsync(DateTime value, CancellationToken cancellationToken)
109 | {
110 | await EnsureDatabaseAsync(cancellationToken);
111 |
112 | return await context.Items
113 | .Where(x => x.CreatedAt > value)
114 | .ToListAsync(cancellationToken);
115 | }
116 |
117 | private async Task EnsureDatabaseAsync(CancellationToken cancellationToken)
118 | => await context.Database.EnsureCreatedAsync(cancellationToken);
119 |
120 | private DateTime UtcNow() => timeProvider.GetUtcNow().UtcDateTime;
121 | }
122 |
--------------------------------------------------------------------------------
/samples/TodoApp/DateTimeExampleProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using MartinCostello.OpenApi;
5 |
6 | namespace TodoApp;
7 |
8 | ///
9 | /// A class representing an example provider for values.
10 | ///
11 | public sealed class DateTimeExampleProvider : IExampleProvider
12 | {
13 | ///
14 | public static DateTime GenerateExample() => new(2025, 03, 05, 16, 43, 44, DateTimeKind.Utc);
15 | }
16 |
--------------------------------------------------------------------------------
/samples/TodoApp/Extensions/DbSetExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace Microsoft.EntityFrameworkCore;
5 |
6 | ///
7 | /// A class containing extension ethods for .
8 | ///
9 | public static class DbSetExtensions
10 | {
11 | ///
12 | /// Finds the item with the specified ID.
13 | ///
14 | /// The type of the item.
15 | /// The type of the key.
16 | /// The set to search for the item in.
17 | /// The key's value.
18 | /// The to use.
19 | ///
20 | /// A representing the asynchronous operation to find the item.
21 | ///
22 | public static ValueTask FindItemAsync(
23 | this DbSet set,
24 | TKey keyValue,
25 | CancellationToken cancellationToken)
26 | where TEntity : class
27 | {
28 | ArgumentNullException.ThrowIfNull(keyValue);
29 | return set.FindAsync([keyValue], cancellationToken);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/samples/TodoApp/GuidExampleProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using MartinCostello.OpenApi;
5 |
6 | namespace TodoApp;
7 |
8 | ///
9 | /// A class representing an example provider for GUIDs.
10 | ///
11 | public sealed class GuidExampleProvider : IExampleProvider
12 | {
13 | private static readonly Guid Value = Guid.Parse("a03952ca-880e-4af7-9cfa-630be0feb4a5");
14 |
15 | ///
16 | public static Guid GenerateExample() => Value;
17 | }
18 |
--------------------------------------------------------------------------------
/samples/TodoApp/Models/CreateTodoItemModel.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using MartinCostello.OpenApi;
5 |
6 | namespace TodoApp.Models;
7 |
8 | ///
9 | /// Represents the model for creating a new Todo item.
10 | ///
11 | [OpenApiExample]
12 | public class CreateTodoItemModel : IExampleProvider
13 | {
14 | ///
15 | /// Gets or sets the text of the Todo item.
16 | ///
17 | public string Text { get; set; } = string.Empty;
18 |
19 | ///
20 | public static CreateTodoItemModel GenerateExample()
21 | {
22 | return new()
23 | {
24 | Text = "Buy eggs 🥚",
25 | };
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/samples/TodoApp/Models/CreatedTodoItemModel.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using MartinCostello.OpenApi;
5 |
6 | namespace TodoApp.Models;
7 |
8 | ///
9 | /// Represents the model for a created Todo item. This class cannot be inherited.
10 | ///
11 | [OpenApiExample]
12 | public class CreatedTodoItemModel : IExampleProvider
13 | {
14 | ///
15 | /// Gets or sets the ID of the created Todo item.
16 | ///
17 | public string Id { get; set; } = string.Empty;
18 |
19 | ///
20 | public static CreatedTodoItemModel GenerateExample()
21 | {
22 | return new()
23 | {
24 | Id = "a03952ca-880e-4af7-9cfa-630be0feb4a5",
25 | };
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/samples/TodoApp/Models/TodoItemFilterModel.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using MartinCostello.OpenApi;
5 |
6 | namespace TodoApp.Models;
7 |
8 | ///
9 | /// Represents the model for searching for Todo items.
10 | ///
11 | [OpenApiExample]
12 | public sealed class TodoItemFilterModel : IExampleProvider
13 | {
14 | ///
15 | /// Gets or sets the text of the filter.
16 | ///
17 | public string Text { get; set; } = string.Empty;
18 |
19 | ///
20 | /// Gets or sets a value indicating whether to search completed Todo items.
21 | ///
22 | public bool IsCompleted { get; set; }
23 |
24 | ///
25 | public static TodoItemFilterModel GenerateExample()
26 | {
27 | return new()
28 | {
29 | IsCompleted = false,
30 | Text = "Buy eggs 🥚",
31 | };
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/samples/TodoApp/Models/TodoItemModel.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using MartinCostello.OpenApi;
5 |
6 | namespace TodoApp.Models;
7 |
8 | ///
9 | /// Represents a Todo item.
10 | ///
11 | [OpenApiExample]
12 | public class TodoItemModel : IExampleProvider
13 | {
14 | ///
15 | /// Gets or sets the ID of the Todo item.
16 | ///
17 | public string Id { get; set; } = default!;
18 |
19 | ///
20 | /// Gets or sets the text of the Todo item.
21 | ///
22 | public string Text { get; set; } = default!;
23 |
24 | ///
25 | /// Gets or sets a value indicating whether the Todo item has been completed.
26 | ///
27 | public bool IsCompleted { get; set; }
28 |
29 | ///
30 | /// Gets or sets the date and time the Todo item was last updated.
31 | ///
32 | public string LastUpdated { get; set; } = default!;
33 |
34 | ///
35 | public static TodoItemModel GenerateExample()
36 | {
37 | return new()
38 | {
39 | Id = "a03952ca-880e-4af7-9cfa-630be0feb4a5",
40 | IsCompleted = false,
41 | Text = "Buy eggs 🥚",
42 | };
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/samples/TodoApp/Models/TodoListViewModel.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using MartinCostello.OpenApi;
5 |
6 | namespace TodoApp.Models;
7 |
8 | ///
9 | /// Represents a collection of Todo items.
10 | ///
11 | [OpenApiExample]
12 | public class TodoListViewModel : IExampleProvider
13 | {
14 | ///
15 | /// Gets or sets the Todo item(s).
16 | ///
17 | public ICollection Items { get; set; } = [];
18 |
19 | ///
20 | public static TodoListViewModel GenerateExample()
21 | {
22 | return new()
23 | {
24 | Items = [TodoItemModel.GenerateExample()],
25 | };
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/samples/TodoApp/ProblemDetailsExampleProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using MartinCostello.OpenApi;
5 | using Microsoft.AspNetCore.Mvc;
6 |
7 | namespace TodoApp;
8 |
9 | ///
10 | /// A class representing an example provider for .
11 | ///
12 | public class ProblemDetailsExampleProvider : IExampleProvider
13 | {
14 | ///
15 | public static ProblemDetails GenerateExample()
16 | {
17 | return new()
18 | {
19 | Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
20 | Title = "Bad Request",
21 | Status = StatusCodes.Status400BadRequest,
22 | Detail = "The specified value is invalid.",
23 | };
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/samples/TodoApp/Program.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using TodoApp;
5 |
6 | // Create the default web application builder
7 | var builder = WebApplication.CreateBuilder(args);
8 |
9 | // Configure the Todo repository and associated services
10 | builder.AddTodoApp();
11 |
12 | // Create the app
13 | var app = builder.Build();
14 |
15 | // Use TodoApp middleware and endpoints with the web application
16 | app.UseTodoApp();
17 |
18 | // Run the application
19 | app.Run();
20 |
21 | namespace TodoApp
22 | {
23 | public partial class Program
24 | {
25 | // Expose the Program class for use with WebApplicationFactory
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/samples/TodoApp/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "TodoApp": {
4 | "commandName": "Project",
5 | "launchBrowser": true,
6 | "applicationUrl": "https://localhost:50001;http://localhost:50000",
7 | "environmentVariables": {
8 | "ASPNETCORE_ENVIRONMENT": "Development"
9 | }
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/samples/TodoApp/Services/ITodoService.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using TodoApp.Models;
5 |
6 | namespace TodoApp.Services;
7 |
8 | ///
9 | /// Represents a service for managing Todo items.
10 | ///
11 | public interface ITodoService
12 | {
13 | ///
14 | /// Adds a new Todo item.
15 | ///
16 | /// The text of the Todo item.
17 | /// The to use.
18 | ///
19 | /// A representing the asynchronous operation that returns the ID of the new Todo item.
20 | ///
21 | Task AddItemAsync(string text, CancellationToken cancellationToken);
22 |
23 | ///
24 | /// Marks a Todo item as completed.
25 | ///
26 | /// The ID of the Todo item to complete.
27 | /// The to use.
28 | ///
29 | /// A representing the asynchronous operation that returns a value indicating whether the Todo item was completed.
30 | ///
31 | Task CompleteItemAsync(Guid itemId, CancellationToken cancellationToken);
32 |
33 | ///
34 | /// Deletes a Todo item.
35 | ///
36 | /// The ID of the Todo item to delete.
37 | /// The to use.
38 | ///
39 | /// A representing the asynchronous operation that returns a value indicating whether the Todo item was deleted.
40 | ///
41 | Task DeleteItemAsync(Guid itemId, CancellationToken cancellationToken);
42 |
43 | ///
44 | /// Gets a Todo item by its ID.
45 | ///
46 | /// The ID of the Todo item.
47 | /// The to use.
48 | ///
49 | /// A representing the asynchronous operation that returns the Todo item, if found; otherwise .
50 | ///
51 | Task GetAsync(Guid itemId, CancellationToken cancellationToken);
52 |
53 | ///
54 | /// Gets a list of Todo items.
55 | ///
56 | /// The to use.
57 | ///
58 | /// A representing the asynchronous operation that returns the list of Todo items.
59 | ///
60 | Task GetListAsync(CancellationToken cancellationToken);
61 |
62 | ///
63 | /// Gets a list of Todo items that match the specified criteria.
64 | ///
65 | /// The to use to search.
66 | /// The to use.
67 | ///
68 | /// A representing the asynchronous operation that returns the list of matching Todo items.
69 | ///
70 | Task FindAsync(TodoItemFilterModel filter, CancellationToken cancellationToken);
71 |
72 | ///
73 | /// Gets a list of Todo items created after the specified date and time.
74 | ///
75 | /// The date and time to look for items created after.
76 | /// The to use.
77 | ///
78 | /// A representing the asynchronous operation that returns the list of matching Todo items.
79 | ///
80 | Task GetAfterDateAsync(DateTime value, CancellationToken cancellationToken);
81 | }
82 |
--------------------------------------------------------------------------------
/samples/TodoApp/Services/TodoService.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using TodoApp.Data;
5 | using TodoApp.Models;
6 |
7 | namespace TodoApp.Services;
8 |
9 | ///
10 | /// A class representing a service for managing Todo items.
11 | ///
12 | /// The to use.
13 | public class TodoService(ITodoRepository repository) : ITodoService
14 | {
15 | ///
16 | public async Task AddItemAsync(string text, CancellationToken cancellationToken)
17 | {
18 | var item = await repository.AddItemAsync(text, cancellationToken);
19 | return item.Id.ToString();
20 | }
21 |
22 | ///
23 | public async Task CompleteItemAsync(Guid itemId, CancellationToken cancellationToken)
24 | {
25 | return await repository.CompleteItemAsync(itemId, cancellationToken);
26 | }
27 |
28 | ///
29 | public async Task DeleteItemAsync(Guid itemId, CancellationToken cancellationToken)
30 | {
31 | return await repository.DeleteItemAsync(itemId, cancellationToken);
32 | }
33 |
34 | ///
35 | public async Task GetAsync(Guid itemId, CancellationToken cancellationToken)
36 | {
37 | var item = await repository.GetItemAsync(itemId, cancellationToken);
38 | return item is null ? null : MapItem(item);
39 | }
40 |
41 | ///
42 | public async Task GetListAsync(CancellationToken cancellationToken)
43 | {
44 | var items = await repository.GetItemsAsync(cancellationToken);
45 | return MapItems(items);
46 | }
47 |
48 | ///
49 | public async Task FindAsync(TodoItemFilterModel filter, CancellationToken cancellationToken)
50 | {
51 | var items = await repository.FindAsync(filter.Text, filter.IsCompleted, cancellationToken);
52 | return MapItems(items);
53 | }
54 |
55 | ///
56 | public async Task GetAfterDateAsync(DateTime value, CancellationToken cancellationToken)
57 | {
58 | var items = await repository.GetAfterDateAsync(value, cancellationToken);
59 | return MapItems(items);
60 | }
61 |
62 | private static TodoListViewModel MapItems(IList items)
63 | {
64 | var result = new List(items.Count);
65 |
66 | foreach (var todo in items)
67 | {
68 | result.Add(MapItem(todo));
69 | }
70 |
71 | return new() { Items = result };
72 | }
73 |
74 | private static TodoItemModel MapItem(TodoItem item)
75 | {
76 | return new()
77 | {
78 | Id = item.Id.ToString(),
79 | IsCompleted = item.CompletedAt.HasValue,
80 | LastUpdated = (item.CompletedAt ?? item.CreatedAt).ToString("u", CultureInfo.InvariantCulture),
81 | Text = item.Text,
82 | };
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/samples/TodoApp/TodoApp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 | true
5 | false
6 | false
7 | $(NoWarn);CA1019;CA1050;CA1813;CS1591;IDE0130
8 | TodoApp
9 | net9.0
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/samples/TodoApp/TodoAppBuilder.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using MartinCostello.OpenApi;
5 | using Microsoft.AspNetCore.HttpOverrides;
6 | using Microsoft.AspNetCore.Mvc;
7 | using Microsoft.AspNetCore.WebUtilities;
8 |
9 | namespace TodoApp;
10 |
11 | ///
12 | /// Extension methods for configuring the TodoApp application.
13 | ///
14 | public static class TodoAppBuilder
15 | {
16 | ///
17 | /// Adds TodoApp services to the specified .
18 | ///
19 | /// The to configure.
20 | ///
21 | /// The value passed by for chaining.
22 | ///
23 | public static WebApplicationBuilder AddTodoApp(this WebApplicationBuilder builder)
24 | {
25 | // Configure the Todo repository and associated services
26 | builder.Services.AddTodoApi();
27 |
28 | // Configure OpenAPI documentation for the Todo API
29 | builder.Services.AddOpenApi(options =>
30 | {
31 | options.AddDocumentTransformer((document, _, _) =>
32 | {
33 | document.Info.Title = "Todo API";
34 | document.Info.Description = "An API for managing Todo items.";
35 | document.Info.Version = "v1";
36 |
37 | document.Info.Contact = new()
38 | {
39 | Name = "martincostello",
40 | Url = new("https://github.com/martincostello/openapi-extensions"),
41 | };
42 |
43 | document.Info.License = new()
44 | {
45 | Name = "Apache 2.0",
46 | Url = new("https://www.apache.org/licenses/LICENSE-2.0"),
47 | };
48 |
49 | return Task.CompletedTask;
50 | });
51 | });
52 |
53 | // Configure extensions for OpenAPI
54 | builder.Services.AddHttpContextAccessor();
55 | builder.Services.AddOpenApiExtensions(options =>
56 | {
57 | // Always return the server URLs in the OpenAPI document
58 | options.AddServerUrls = true;
59 |
60 | // Set a default URL to use for generation of the OpenAPI document using
61 | // https://www.nuget.org/packages/Microsoft.Extensions.ApiDescription.Server.
62 | options.DefaultServerUrl = "https://localhost:50001";
63 |
64 | // Add examples for OpenAPI operations and components
65 | options.AddExamples = true;
66 |
67 | // Add JSON serialization context to use to serialize examples
68 | options.SerializationContexts.Add(TodoJsonSerializerContext.Default);
69 |
70 | // Add custom example providers for DateTimes, GUIDs and ProblemDetails
71 | options.AddExample();
72 | options.AddExample();
73 | options.AddExample();
74 |
75 | // Configure XML comments for the schemas in the OpenAPI document
76 | options.AddXmlComments();
77 |
78 | // Add a custom transformation for the descriptions in the OpenAPI document
79 | options.DescriptionTransformations.Add((p) => p.Replace(" This class cannot be inherited.", string.Empty, StringComparison.Ordinal));
80 | });
81 |
82 | if (!builder.Environment.IsDevelopment())
83 | {
84 | builder.Services.AddProblemDetails();
85 | builder.Services.Configure((options) =>
86 | {
87 | options.CustomizeProblemDetails = (context) =>
88 | {
89 | if (context.Exception is not null)
90 | {
91 | context.ProblemDetails.Detail = "An internal error occurred.";
92 | }
93 |
94 | context.ProblemDetails.Instance = context.HttpContext.Request.Path;
95 | context.ProblemDetails.Title = ReasonPhrases.GetReasonPhrase(context.ProblemDetails.Status ?? StatusCodes.Status500InternalServerError);
96 | };
97 | });
98 | }
99 |
100 | if (string.Equals(builder.Configuration["CODESPACES"], "true", StringComparison.OrdinalIgnoreCase))
101 | {
102 | // When running in GitHub Codespaces, X-Forwarded-Host also needs to be set
103 | builder.Services.Configure(
104 | options => options.ForwardedHeaders |= ForwardedHeaders.XForwardedHost);
105 | }
106 |
107 | return builder;
108 | }
109 |
110 | ///
111 | /// Configures the specified to use TodoApp.
112 | ///
113 | /// The to configure.
114 | ///
115 | /// The value passed by for chaining.
116 | ///
117 | public static WebApplication UseTodoApp(this WebApplication app)
118 | {
119 | // Configure error handling
120 | app.UseStatusCodePagesWithReExecute("/error", "?id={0}");
121 |
122 | // Require use of HTTPS in production
123 | if (!app.Environment.IsDevelopment())
124 | {
125 | app.UseHsts();
126 | app.UseHttpsRedirection();
127 | }
128 |
129 | // Add static files for SwaggerUI
130 | app.UseStaticFiles();
131 |
132 | // Add endpoints for OpenAPI
133 | app.MapOpenApi();
134 | app.MapOpenApiYaml();
135 |
136 | // Add the HTTP endpoints
137 | app.MapTodoApiRoutes();
138 |
139 | return app;
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/samples/TodoApp/TodoJsonSerializerContext.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Text.Json.Nodes;
5 | using System.Text.Json.Serialization;
6 | using Microsoft.AspNetCore.Mvc;
7 | using TodoApp.Models;
8 |
9 | namespace TodoApp;
10 |
11 | ///
12 | /// A class that provides metadata for (de)serializing JSON for both the API endpoints and with OpenAPI.
13 | ///
14 | [JsonSerializable(typeof(CreateTodoItemModel))]
15 | [JsonSerializable(typeof(CreatedTodoItemModel))]
16 | [JsonSerializable(typeof(DateTime))]
17 | [JsonSerializable(typeof(Guid))]
18 | [JsonSerializable(typeof(JsonObject))]
19 | [JsonSerializable(typeof(ProblemDetails))]
20 | [JsonSerializable(typeof(TodoItemFilterModel))]
21 | [JsonSerializable(typeof(TodoItemModel))]
22 | [JsonSerializable(typeof(TodoListViewModel))]
23 | [JsonSourceGenerationOptions(
24 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
25 | NumberHandling = JsonNumberHandling.Strict,
26 | PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
27 | WriteIndented = true)]
28 | public sealed partial class TodoJsonSerializerContext : JsonSerializerContext;
29 |
--------------------------------------------------------------------------------
/samples/TodoApp/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "System": "Warning",
6 | "Microsoft": "Warning",
7 | "Microsoft.Hosting.Lifetime": "Information"
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/samples/TodoApp/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Warning"
5 | }
6 | },
7 | "AllowedHosts": "*",
8 | "DataDirectory": "App_Data"
9 | }
10 |
--------------------------------------------------------------------------------
/samples/TodoApp/wwwroot/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martincostello/openapi-extensions/ce1cc678ff86186a2ad58f910f3e183796449a21/samples/TodoApp/wwwroot/favicon.ico
--------------------------------------------------------------------------------
/samples/TodoApp/wwwroot/swagger-ui/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | API Documentation - TodoApp
6 |
7 |
8 |
30 |
31 |
32 |
33 |
34 |
35 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/ExampleFormatter.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.OpenApi;
5 |
6 | ///
7 | /// A class containing methods to help format JSON examples for OpenAPI. This class cannot be inherited.
8 | ///
9 | internal static partial class ExampleFormatter
10 | {
11 | }
12 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/ExampleFormatter.net9.0.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | #if NET9_0
5 |
6 | using System.Diagnostics.CodeAnalysis;
7 | using System.Text.Json;
8 | using System.Text.Json.Serialization;
9 | using Microsoft.OpenApi.Any;
10 |
11 | namespace MartinCostello.OpenApi;
12 |
13 | internal static partial class ExampleFormatter
14 | {
15 | ///
16 | /// Formats the specified value as JSON.
17 | ///
18 | /// The type of the value.
19 | /// The example value to format as JSON.
20 | /// The JSON serializer context to use.
21 | ///
22 | /// The to use as the example.
23 | ///
24 | ///
25 | /// is .
26 | ///
27 | public static IOpenApiAny AsJson(T example, JsonSerializerContext context)
28 | {
29 | ArgumentNullException.ThrowIfNull(context);
30 |
31 | string? json = JsonSerializer.Serialize(example, typeof(T), context);
32 | using var document = JsonDocument.Parse(json);
33 |
34 | return TryParse(document.RootElement, out var any) ? any : new OpenApiNull();
35 | }
36 |
37 | private static bool TryParse(JsonElement token, [NotNullWhen(true)] out IOpenApiAny? any)
38 | {
39 | any = null;
40 |
41 | switch (token.ValueKind)
42 | {
43 | case JsonValueKind.Array:
44 | var array = new OpenApiArray();
45 |
46 | foreach (var value in token.EnumerateArray())
47 | {
48 | if (TryParse(value, out var child))
49 | {
50 | array.Add(child);
51 | }
52 | }
53 |
54 | any = array;
55 | return true;
56 |
57 | case JsonValueKind.False:
58 | any = new OpenApiBoolean(false);
59 | return true;
60 |
61 | case JsonValueKind.True:
62 | any = new OpenApiBoolean(true);
63 | return true;
64 |
65 | case JsonValueKind.Number:
66 | any = new OpenApiDouble(token.GetDouble());
67 | return true;
68 |
69 | case JsonValueKind.String:
70 | any = new OpenApiString(token.GetString());
71 | return true;
72 |
73 | case JsonValueKind.Object:
74 | var obj = new OpenApiObject();
75 |
76 | foreach (var child in token.EnumerateObject())
77 | {
78 | if (TryParse(child.Value, out var value))
79 | {
80 | obj[child.Name] = value;
81 | }
82 | }
83 |
84 | any = obj;
85 | return true;
86 |
87 | case JsonValueKind.Null:
88 | any = new OpenApiNull();
89 | return true;
90 |
91 | case JsonValueKind.Undefined:
92 | default:
93 | return false;
94 | }
95 | }
96 | }
97 | #endif
98 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/ExampleTargets.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.OpenApi;
5 |
6 | ///
7 | /// A class that defines the valid attribute targets for example attributes.
8 | ///
9 | internal static class ExampleTargets
10 | {
11 | ///
12 | /// The valid attribute targets for example attributes.
13 | ///
14 | public const AttributeTargets ValidTargets =
15 | AttributeTargets.Class |
16 | AttributeTargets.Enum |
17 | AttributeTargets.Interface |
18 | AttributeTargets.Method |
19 | AttributeTargets.Parameter |
20 | AttributeTargets.ReturnValue |
21 | AttributeTargets.Struct;
22 | }
23 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/IExampleProvider`1.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.OpenApi;
5 |
6 | ///
7 | /// Defines a method for obtaining examples for use in OpenAPI documentation.
8 | ///
9 | /// The type of the example.
10 | public interface IExampleProvider
11 | {
12 | ///
13 | /// Generates the example to use.
14 | ///
15 | ///
16 | /// A that should be used as the example.
17 | ///
18 | static abstract T GenerateExample();
19 | }
20 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/IOpenApiExampleMetadata.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.OpenApi;
5 |
6 | ///
7 | /// Defines metadata for an OpenAPI example.
8 | ///
9 | public partial interface IOpenApiExampleMetadata
10 | {
11 | ///
12 | /// Gets the type associated with the example.
13 | ///
14 | Type ExampleType { get; }
15 | }
16 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/IOpenApiExampleMetadata.net9.0.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | #if NET9_0
5 |
6 | using System.Text.Json.Serialization;
7 | using Microsoft.OpenApi.Any;
8 |
9 | namespace MartinCostello.OpenApi;
10 |
11 | public partial interface IOpenApiExampleMetadata
12 | {
13 | ///
14 | /// Generates an example object for the type associated with .
15 | ///
16 | /// The JSON serializer context to use.
17 | ///
18 | /// The example to use.
19 | ///
20 | IOpenApiAny GenerateExample(JsonSerializerContext context);
21 | }
22 |
23 | #endif
24 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/MartinCostello.OpenApi.Extensions.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | OpenAPI Extensions
4 | Extensions for ASP.NET Core's OpenAPI support.
5 | true
6 | true
7 | true
8 | $(InterceptorsPreviewNamespaces);Microsoft.AspNetCore.Http.Generated
9 | true
10 | true
11 | Library
12 | MartinCostello.OpenApi.Extensions
13 | MartinCostello.OpenApi
14 | $(Description)
15 | net9.0
16 | $(AssemblyTitle)
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/OpenApiConstants.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.OpenApi;
5 |
6 | internal static class OpenApiConstants
7 | {
8 | // See https://github.com/dotnet/aspnetcore/blob/0fee04e2e1e507f0c993fa902d53abdd7c5dff65/src/OpenApi/src/Services/OpenApiConstants.cs#L10
9 | internal const string DefaultDocumentName = "v1";
10 |
11 | // See https://github.com/dotnet/aspnetcore/blob/0fee04e2e1e507f0c993fa902d53abdd7c5dff65/src/OpenApi/src/Services/OpenApiConstants.cs#L12
12 | internal const string DefaultOpenApiRouteAsYaml = "/openapi/{documentName}.yaml";
13 |
14 | // See https://github.com/dotnet/aspnetcore/blob/0fee04e2e1e507f0c993fa902d53abdd7c5dff65/src/OpenApi/src/Services/OpenApiConstants.cs#L13
15 | internal const string DescriptionId = "x-aspnetcore-id";
16 |
17 | // See https://github.com/dotnet/aspnetcore/blob/0fee04e2e1e507f0c993fa902d53abdd7c5dff65/src/OpenApi/src/Services/OpenApiConstants.cs#L14
18 | internal const string SchemaId = "x-schema-id";
19 | }
20 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/OpenApiEndpointConventionBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using Microsoft.AspNetCore.Builder;
5 |
6 | namespace MartinCostello.OpenApi;
7 |
8 | ///
9 | /// A class containing methods for adding routing metadata to endpoint
10 | /// instances for OpenAPI using .
11 | /// This class cannot be inherited.
12 | ///
13 | public static class OpenApiEndpointConventionBuilderExtensions
14 | {
15 | ///
16 | /// Adds OpenAPI response metadata to for all builders produced by builder.
17 | ///
18 | /// The type of the endpoint convention builder.
19 | /// The .
20 | /// The HTTP status code for the response.
21 | /// The description of the response.
22 | ///
23 | /// The current instance for chaining.
24 | ///
25 | ///
26 | /// is .
27 | ///
28 | public static TBuilder ProducesOpenApiResponse(this TBuilder builder, int statusCode, string description)
29 | where TBuilder : IEndpointConventionBuilder
30 | {
31 | ArgumentNullException.ThrowIfNull(builder);
32 | return builder.WithMetadata(new OpenApiResponseAttribute(statusCode, description));
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/OpenApiExampleAttribute.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.OpenApi;
5 |
6 | ///
7 | /// An attribute representing a string example for an OpenAPI document. This class cannot be inherited.
8 | ///
9 | /// The example's value.
10 | [AttributeUsage(ExampleTargets.ValidTargets, AllowMultiple = false)]
11 | public sealed class OpenApiExampleAttribute(string value) : OpenApiExampleAttribute, IExampleProvider
12 | {
13 | ///
14 | /// Gets the example value.
15 | ///
16 | public string Value { get; } = value;
17 |
18 | ///
19 | static string IExampleProvider.GenerateExample() => "value";
20 |
21 | ///
22 | public override string GenerateExample() => Value;
23 | }
24 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/OpenApiExampleAttribute`1.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.OpenApi;
5 |
6 | #pragma warning disable CA1813
7 |
8 | ///
9 | /// An attribute representing an example for an OpenAPI document.
10 | ///
11 | /// The type of the example.
12 | public class OpenApiExampleAttribute() : OpenApiExampleAttribute()
13 | where T : IExampleProvider;
14 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/OpenApiExampleAttribute`2.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.OpenApi;
5 |
6 | #pragma warning disable CA1813
7 |
8 | ///
9 | /// An attribute representing an example for an OpenAPI document.
10 | ///
11 | /// The type of the schema.
12 | /// The type of the example provider.
13 | [AttributeUsage(ExampleTargets.ValidTargets, AllowMultiple = true, Inherited = true)]
14 | public partial class OpenApiExampleAttribute : Attribute, IOpenApiExampleMetadata
15 | where TProvider : IExampleProvider
16 | {
17 | ///
18 | public Type ExampleType { get; } = typeof(TSchema);
19 |
20 | ///
21 | /// Generates the example to use.
22 | ///
23 | ///
24 | /// A that should be used as the example.
25 | ///
26 | public virtual TSchema GenerateExample() => TProvider.GenerateExample();
27 | }
28 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/OpenApiExampleAttribute`2.net9.0.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | #if NET9_0
5 |
6 | using System.Text.Json.Serialization;
7 | using Microsoft.OpenApi.Any;
8 |
9 | namespace MartinCostello.OpenApi;
10 |
11 | public partial class OpenApiExampleAttribute : Attribute, IOpenApiExampleMetadata
12 | where TProvider : IExampleProvider
13 | {
14 | ///
15 | IOpenApiAny IOpenApiExampleMetadata.GenerateExample(JsonSerializerContext context)
16 | => ExampleFormatter.AsJson(GenerateExample(), context);
17 | }
18 |
19 | #endif
20 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/OpenApiResponseAttribute.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.OpenApi;
5 |
6 | ///
7 | /// Represents an OpenAPI operation response. This class cannot be inherited.
8 | ///
9 | /// The HTTP status code for the response.
10 | /// The description of the response.
11 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
12 | public sealed class OpenApiResponseAttribute(int httpStatusCode, string description) : Attribute
13 | {
14 | ///
15 | /// Gets the HTTP status code for the response.
16 | ///
17 | public int HttpStatusCode { get; } = httpStatusCode;
18 |
19 | ///
20 | /// Gets the description of the response.
21 | ///
22 | public string Description { get; } = description;
23 | }
24 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/PublicAPI/PublicAPI.Shipped.txt:
--------------------------------------------------------------------------------
1 | #nullable enable
2 | MartinCostello.OpenApi.IExampleProvider
3 | MartinCostello.OpenApi.IExampleProvider.GenerateExample() -> T
4 | MartinCostello.OpenApi.IOpenApiExampleMetadata
5 | MartinCostello.OpenApi.IOpenApiExampleMetadata.ExampleType.get -> System.Type!
6 | MartinCostello.OpenApi.OpenApiEndpointConventionBuilderExtensions
7 | MartinCostello.OpenApi.OpenApiExampleAttribute
8 | MartinCostello.OpenApi.OpenApiExampleAttribute.OpenApiExampleAttribute(string! value) -> void
9 | MartinCostello.OpenApi.OpenApiExampleAttribute.Value.get -> string!
10 | MartinCostello.OpenApi.OpenApiExampleAttribute
11 | MartinCostello.OpenApi.OpenApiExampleAttribute.OpenApiExampleAttribute() -> void
12 | MartinCostello.OpenApi.OpenApiExampleAttribute
13 | MartinCostello.OpenApi.OpenApiExampleAttribute.ExampleType.get -> System.Type!
14 | MartinCostello.OpenApi.OpenApiExampleAttribute.OpenApiExampleAttribute() -> void
15 | MartinCostello.OpenApi.OpenApiExtensions
16 | MartinCostello.OpenApi.OpenApiExtensionsOptions
17 | MartinCostello.OpenApi.OpenApiExtensionsOptions.AddExample() -> MartinCostello.OpenApi.OpenApiExtensionsOptions!
18 | MartinCostello.OpenApi.OpenApiExtensionsOptions.AddExample() -> MartinCostello.OpenApi.OpenApiExtensionsOptions!
19 | MartinCostello.OpenApi.OpenApiExtensionsOptions.AddExamples.get -> bool
20 | MartinCostello.OpenApi.OpenApiExtensionsOptions.AddExamples.set -> void
21 | MartinCostello.OpenApi.OpenApiExtensionsOptions.AddServerUrls.get -> bool
22 | MartinCostello.OpenApi.OpenApiExtensionsOptions.AddServerUrls.set -> void
23 | MartinCostello.OpenApi.OpenApiExtensionsOptions.AddXmlComments() -> MartinCostello.OpenApi.OpenApiExtensionsOptions!
24 | MartinCostello.OpenApi.OpenApiExtensionsOptions.DefaultServerUrl.get -> string?
25 | MartinCostello.OpenApi.OpenApiExtensionsOptions.DefaultServerUrl.set -> void
26 | MartinCostello.OpenApi.OpenApiExtensionsOptions.DescriptionTransformations.get -> System.Collections.Generic.IList!>!
27 | MartinCostello.OpenApi.OpenApiExtensionsOptions.ExamplesMetadata.get -> System.Collections.Generic.ICollection!
28 | MartinCostello.OpenApi.OpenApiExtensionsOptions.OpenApiExtensionsOptions() -> void
29 | MartinCostello.OpenApi.OpenApiExtensionsOptions.SerializationContexts.get -> System.Collections.Generic.IList!
30 | MartinCostello.OpenApi.OpenApiExtensionsOptions.XmlDocumentationAssemblies.get -> System.Collections.Generic.IList!
31 | MartinCostello.OpenApi.OpenApiResponseAttribute
32 | MartinCostello.OpenApi.OpenApiResponseAttribute.Description.get -> string!
33 | MartinCostello.OpenApi.OpenApiResponseAttribute.HttpStatusCode.get -> int
34 | MartinCostello.OpenApi.OpenApiResponseAttribute.OpenApiResponseAttribute(int httpStatusCode, string! description) -> void
35 | override MartinCostello.OpenApi.OpenApiExampleAttribute.GenerateExample() -> string!
36 | static MartinCostello.OpenApi.OpenApiEndpointConventionBuilderExtensions.ProducesOpenApiResponse(this TBuilder builder, int statusCode, string! description) -> TBuilder
37 | static MartinCostello.OpenApi.OpenApiExtensions.AddOpenApiExtensions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
38 | static MartinCostello.OpenApi.OpenApiExtensions.AddOpenApiExtensions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, string! documentName) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
39 | static MartinCostello.OpenApi.OpenApiExtensions.AddOpenApiExtensions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, string! documentName, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
40 | static MartinCostello.OpenApi.OpenApiExtensions.AddOpenApiExtensions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, string! documentName, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
41 | static MartinCostello.OpenApi.OpenApiExtensions.AddOpenApiExtensions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
42 | static MartinCostello.OpenApi.OpenApiExtensions.AddOpenApiExtensions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
43 | virtual MartinCostello.OpenApi.OpenApiExampleAttribute.GenerateExample() -> TSchema
44 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/PublicAPI/PublicAPI.Unshipped.txt:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/PublicAPI/net9.0/PublicAPI.Shipped.txt:
--------------------------------------------------------------------------------
1 | #nullable enable
2 | MartinCostello.OpenApi.IOpenApiExampleMetadata.GenerateExample(System.Text.Json.Serialization.JsonSerializerContext! context) -> Microsoft.OpenApi.Any.IOpenApiAny!
3 | MartinCostello.OpenApi.OpenApiEndpointRouteBuilderExtensions
4 | static MartinCostello.OpenApi.OpenApiEndpointRouteBuilderExtensions.MapOpenApiYaml(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern = "/openapi/{documentName}.yaml") -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder!
5 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/PublicAPI/net9.0/PublicAPI.Unshipped.txt:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/ReflectionExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Reflection;
5 |
6 | namespace MartinCostello.OpenApi;
7 |
8 | internal static class ReflectionExtensions
9 | {
10 | public static IEnumerable GetExampleMetadata(this MethodInfo method)
11 | => method.GetCustomAttributes()
12 | .OfType()
13 | .Concat(method.ReturnParameter.GetExampleMetadata());
14 |
15 | public static IEnumerable GetExampleMetadata(this ParameterInfo parameter)
16 | => parameter.GetCustomAttributes().OfType();
17 |
18 | public static IOpenApiExampleMetadata? GetExampleMetadata(this Type type)
19 | => type.GetCustomAttributes(inherit: true)
20 | .OfType()
21 | .FirstOrDefault((p) => p.ExampleType.IsAssignableFrom(type));
22 | }
23 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/Transformers/AddOperationXmlDocumentationTransformer.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Reflection;
5 | using Microsoft.AspNetCore.Mvc.ApiExplorer;
6 | using Microsoft.AspNetCore.Mvc.Controllers;
7 | using Microsoft.AspNetCore.Mvc.Infrastructure;
8 | using Microsoft.AspNetCore.OpenApi;
9 | using Microsoft.OpenApi.Models;
10 |
11 | namespace MartinCostello.OpenApi.Transformers;
12 |
13 | ///
14 | /// A class that adds XML documentation for OpenAPI operations. This class cannot be inherited.
15 | ///
16 | /// The to use.
17 | internal sealed class AddOperationXmlDocumentationTransformer(XmlDescriptionService service) : IOpenApiOperationTransformer
18 | {
19 | private readonly XmlDescriptionService _service = service;
20 |
21 | ///
22 | public Task TransformAsync(
23 | OpenApiOperation operation,
24 | OpenApiOperationTransformerContext context,
25 | CancellationToken cancellationToken)
26 | {
27 | ApplyOperationDescription(operation, context);
28 | ApplyParametersDescription(operation, context);
29 |
30 | return Task.CompletedTask;
31 | }
32 |
33 | private static string? GetXmlMethodName(OpenApiOperationTransformerContext context) =>
34 | GetMethodInfo(context.Description) is not { } methodInfo ||
35 | XmlCommentsHelper.GetMemberName(methodInfo) is not { Length: > 0 } name ? null : name;
36 |
37 | private static MethodInfo? GetMethodInfo(ApiDescription description)
38 | {
39 | if (description.ActionDescriptor is ControllerActionDescriptor descriptor)
40 | {
41 | return descriptor.MethodInfo;
42 | }
43 |
44 | return description.ActionDescriptor.EndpointMetadata.OfType().FirstOrDefault();
45 | }
46 |
47 | private static bool TryApplyParameterDescription(
48 | OpenApiOperation operation,
49 | ApiParameterDescription parameterDescription,
50 | string description)
51 | {
52 | var parameter = operation.Parameters?.FirstOrDefault((p) => p.Name == parameterDescription.Name);
53 |
54 | if (parameter is null)
55 | {
56 | return false;
57 | }
58 |
59 | parameter.Description ??= description;
60 |
61 | return true;
62 | }
63 |
64 | private void ApplyOperationDescription(
65 | OpenApiOperation operation,
66 | OpenApiOperationTransformerContext context)
67 | {
68 | if (GetXmlMethodName(context) is not { Length: > 0 } methodName)
69 | {
70 | return;
71 | }
72 |
73 | if (operation.Summary is null &&
74 | _service.GetDescription(methodName) is { Length: > 0 } summary)
75 | {
76 | operation.Summary = summary;
77 | }
78 |
79 | if (operation.Description is null &&
80 | _service.GetDescription(methodName, section: "remarks") is { Length: > 0 } remarks)
81 | {
82 | operation.Description = remarks;
83 | }
84 | }
85 |
86 | private void ApplyParametersDescription(
87 | OpenApiOperation operation,
88 | OpenApiOperationTransformerContext context)
89 | {
90 | if (operation.Parameters is null)
91 | {
92 | return;
93 | }
94 |
95 | foreach (var description in context.Description.ParameterDescriptions)
96 | {
97 | if (TryApplyModelParameterDescription(operation, description))
98 | {
99 | continue;
100 | }
101 |
102 | TryApplyEndpointParameterDescription(operation, context, description);
103 | }
104 | }
105 |
106 | private bool TryApplyModelParameterDescription(
107 | OpenApiOperation operation,
108 | ApiParameterDescription description)
109 | {
110 | if (description.ParameterDescriptor is not IParameterInfoParameterDescriptor descriptor ||
111 | XmlCommentsHelper.GetMemberName(descriptor.ParameterInfo.Member) is not { Length: > 0 } name ||
112 | _service.GetDescription(name) is not { Length: > 0 } summary)
113 | {
114 | return false;
115 | }
116 |
117 | return TryApplyParameterDescription(operation, description, summary);
118 | }
119 |
120 | private bool TryApplyEndpointParameterDescription(
121 | OpenApiOperation operation,
122 | OpenApiOperationTransformerContext context,
123 | ApiParameterDescription description)
124 | {
125 | if (GetXmlMethodName(context) is { Length: > 0 } methodName &&
126 | _service.GetDescription(methodName, description.Name) is { Length: > 0 } summary)
127 | {
128 | return TryApplyParameterDescription(operation, description, summary);
129 | }
130 |
131 | return false;
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/Transformers/AddParameterDescriptionsTransformer.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Collections.Concurrent;
5 | using System.ComponentModel;
6 | using System.Reflection;
7 | using Microsoft.AspNetCore.Mvc.ApiExplorer;
8 | using Microsoft.AspNetCore.OpenApi;
9 | using Microsoft.OpenApi.Models;
10 |
11 | namespace MartinCostello.OpenApi.Transformers;
12 |
13 | ///
14 | /// A class that adds descriptions to OpenAPI operations. This class cannot be inherited.
15 | ///
16 | internal sealed class AddParameterDescriptionsTransformer : IOpenApiOperationTransformer
17 | {
18 | private readonly ConcurrentDictionary _methodParametersDescriptions = [];
19 |
20 | ///
21 | public Task TransformAsync(
22 | OpenApiOperation operation,
23 | OpenApiOperationTransformerContext context,
24 | CancellationToken cancellationToken)
25 | {
26 | if (operation.Parameters is { Count: > 0 })
27 | {
28 | TryAddParameterDescriptions(operation.Parameters, context.Description);
29 | }
30 |
31 | return Task.CompletedTask;
32 | }
33 |
34 | private void TryAddParameterDescriptions(
35 | IList parameters,
36 | ApiDescription apiDescription)
37 | {
38 | var descriptions = GetMethodParameterDescriptions(apiDescription);
39 |
40 | if (descriptions is { Length: > 0 })
41 | {
42 | foreach ((var argument, var description) in descriptions)
43 | {
44 | if (description is not null)
45 | {
46 | var parameter = parameters.FirstOrDefault((p) => p.Name == argument.Name);
47 | if (parameter is not null)
48 | {
49 | parameter.Description ??= description;
50 | }
51 | }
52 | }
53 | }
54 | }
55 |
56 | private ParameterDescription[] GetMethodParameterDescriptions(ApiDescription description)
57 | {
58 | var method = description.ActionDescriptor.EndpointMetadata.OfType().FirstOrDefault();
59 |
60 | if (method is null)
61 | {
62 | return [];
63 | }
64 |
65 | return _methodParametersDescriptions.GetOrAdd(method, static (p) =>
66 | {
67 | var parameters = p.GetParameters();
68 | var descriptions = new ParameterDescription[parameters.Length];
69 |
70 | for (int i = 0; i < parameters.Length; i++)
71 | {
72 | var parameter = parameters[i];
73 | var description = p.GetCustomAttributes().FirstOrDefault()?.Description;
74 |
75 | descriptions[i] = new(parameter, description);
76 | }
77 |
78 | return descriptions;
79 | });
80 | }
81 |
82 | private sealed record ParameterDescription(ParameterInfo Parameter, string? Description);
83 | }
84 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/Transformers/AddResponseDescriptionsTransformer.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using Microsoft.AspNetCore.OpenApi;
5 | using Microsoft.OpenApi.Models;
6 |
7 | namespace MartinCostello.OpenApi.Transformers;
8 |
9 | ///
10 | /// A class that adds response descriptions to OpenAPI operations. This class cannot be inherited.
11 | ///
12 | internal sealed class AddResponseDescriptionsTransformer : IOpenApiOperationTransformer
13 | {
14 | ///
15 | public Task TransformAsync(
16 | OpenApiOperation operation,
17 | OpenApiOperationTransformerContext context,
18 | CancellationToken cancellationToken)
19 | {
20 | foreach (var attribute in context.Description.ActionDescriptor.EndpointMetadata.OfType())
21 | {
22 | if (operation.Responses is not null &&
23 | operation.Responses.TryGetValue(attribute.HttpStatusCode.ToString(CultureInfo.InvariantCulture), out var response))
24 | {
25 | response.Description = attribute.Description;
26 | }
27 | }
28 |
29 | return Task.CompletedTask;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/Transformers/AddSchemaXmlDocumentationTransformer.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Reflection;
5 | using System.Text.Json.Serialization.Metadata;
6 | using Microsoft.AspNetCore.OpenApi;
7 | using Microsoft.OpenApi.Models;
8 |
9 | namespace MartinCostello.OpenApi.Transformers;
10 |
11 | ///
12 | /// A class that adds XML documentation to OpenAPI schemas. This class cannot be inherited.
13 | ///
14 | /// The assembly to add XML documentation to the types of.
15 | /// The to use.
16 | internal sealed class AddSchemaXmlDocumentationTransformer(Assembly assembly, XmlDescriptionService service)
17 | : IOpenApiSchemaTransformer
18 | {
19 | private readonly Assembly _assembly = assembly;
20 | private readonly XmlDescriptionService _service = service;
21 |
22 | ///
23 | public Task TransformAsync(
24 | OpenApiSchema schema,
25 | OpenApiSchemaTransformerContext context,
26 | CancellationToken cancellationToken)
27 | {
28 | if (schema.Description is null &&
29 | GetMemberName(context.JsonTypeInfo, context.JsonPropertyInfo) is { Length: > 0 } memberName &&
30 | _service.GetDescription(memberName) is { Length: > 0 } description)
31 | {
32 | schema.Description = description;
33 | }
34 |
35 | return Task.CompletedTask;
36 | }
37 |
38 | private string? GetMemberName(JsonTypeInfo typeInfo, JsonPropertyInfo? propertyInfo)
39 | {
40 | if (typeInfo.Type.Assembly != _assembly &&
41 | propertyInfo?.DeclaringType.Assembly != _assembly)
42 | {
43 | return null;
44 | }
45 | else if (propertyInfo is not null)
46 | {
47 | var typeName = propertyInfo.DeclaringType.FullName;
48 | var memberName =
49 | propertyInfo.AttributeProvider is MemberInfo member ?
50 | member.Name :
51 | $"{char.ToUpperInvariant(propertyInfo.Name[0])}{propertyInfo.Name[1..]}";
52 |
53 | var memberType = propertyInfo.AttributeProvider is PropertyInfo ? "P" : "F";
54 |
55 | return $"{memberType}:{typeName}{Type.Delimiter}{memberName}";
56 | }
57 | else
58 | {
59 | return $"T:{typeInfo.Type.FullName}";
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/Transformers/AddServersTransformer.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using Microsoft.AspNetCore.Builder;
5 | using Microsoft.AspNetCore.Http;
6 | using Microsoft.AspNetCore.OpenApi;
7 | using Microsoft.Extensions.Options;
8 | using Microsoft.OpenApi.Models;
9 |
10 | namespace MartinCostello.OpenApi.Transformers;
11 |
12 | ///
13 | /// A class that server information to an OpenAPI document. This class cannot be inherited.
14 | ///
15 | /// The configured .
16 | /// The to use, if available.
17 | /// The configured , if any.
18 | internal sealed class AddServersTransformer(
19 | IOptions extensionsOptions,
20 | IHttpContextAccessor? accessor,
21 | IOptions? forwardedHeadersOptions) : IOpenApiDocumentTransformer
22 | {
23 | ///
24 | public Task TransformAsync(
25 | OpenApiDocument document,
26 | OpenApiDocumentTransformerContext context,
27 | CancellationToken cancellationToken)
28 | {
29 | if (GetServerUrl() is { } url)
30 | {
31 | document.Servers = [new() { Url = url }];
32 | }
33 |
34 | return Task.CompletedTask;
35 | }
36 |
37 | private string? GetServerUrl()
38 | {
39 | if (!extensionsOptions.Value.AddServerUrls)
40 | {
41 | return null;
42 | }
43 |
44 | if (accessor?.HttpContext?.Request is not { } request)
45 | {
46 | return extensionsOptions.Value.DefaultServerUrl;
47 | }
48 |
49 | if (forwardedHeadersOptions?.Value is not { } options)
50 | {
51 | return null;
52 | }
53 |
54 | var scheme = TryGetFirstHeader(options.ForwardedProtoHeaderName) ?? request.Scheme;
55 | var host = TryGetFirstHeader(options.ForwardedHostHeaderName) ?? request.Host.ToString();
56 |
57 | return new Uri($"{scheme}://{host}").ToString().TrimEnd('/');
58 |
59 | string? TryGetFirstHeader(string name)
60 | => request.Headers.TryGetValue(name, out var values) ? values.FirstOrDefault() : null;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/Transformers/DescriptionsTransformer.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using Microsoft.AspNetCore.OpenApi;
5 | using Microsoft.OpenApi.Models;
6 |
7 | namespace MartinCostello.OpenApi.Transformers;
8 |
9 | ///
10 | /// A class representing an operation and schema description transformer. This class cannot be inherited.
11 | ///
12 | /// A delegate to a method to use to transform description strings.
13 | internal sealed class DescriptionsTransformer(Func transformer) :
14 | IOpenApiOperationTransformer,
15 | IOpenApiSchemaTransformer
16 | {
17 | ///
18 | public Task TransformAsync(
19 | OpenApiOperation operation,
20 | OpenApiOperationTransformerContext context,
21 | CancellationToken cancellationToken)
22 | {
23 | ApplyResponseDescriptions(operation);
24 | ApplyParametersDescriptions(operation);
25 |
26 | return Task.CompletedTask;
27 | }
28 |
29 | ///
30 | public Task TransformAsync(
31 | OpenApiSchema schema,
32 | OpenApiSchemaTransformerContext context,
33 | CancellationToken cancellationToken)
34 | {
35 | if (schema.Description is { } schemaDescription)
36 | {
37 | schema.Description = transformer(schemaDescription);
38 | }
39 |
40 | if (schema.Properties is { } properties)
41 | {
42 | foreach (var property in properties.Values)
43 | {
44 | if (property.Description is { } propertyDescription)
45 | {
46 | property.Description = transformer(propertyDescription);
47 | }
48 | }
49 | }
50 |
51 | return Task.CompletedTask;
52 | }
53 |
54 | internal static string RemoveBackticks(string description)
55 | => description.Replace("`", string.Empty, StringComparison.Ordinal);
56 |
57 | internal static string RemoveStyleCopPrefixes(string description)
58 | {
59 | string[] prefixes =
60 | [
61 | "Gets or sets a value indicating ",
62 | "Gets a value indicating ",
63 | "Gets or sets ",
64 | "Gets ",
65 | ];
66 |
67 | foreach (var prefix in prefixes)
68 | {
69 | description = description.Replace(prefix, string.Empty, StringComparison.Ordinal);
70 | }
71 |
72 | description = char.ToUpperInvariant(description[0]) + description[1..];
73 |
74 | return description;
75 | }
76 |
77 | private void ApplyResponseDescriptions(OpenApiOperation operation)
78 | {
79 | if (operation.Responses is { } responses)
80 | {
81 | foreach (var response in responses.Values)
82 | {
83 | if (response.Content is not { } content)
84 | {
85 | continue;
86 | }
87 |
88 | foreach (var model in content.Values)
89 | {
90 | if (model.Schema?.Properties is { } properties)
91 | {
92 | foreach (var property in properties.Values)
93 | {
94 | if (property.Description is { } description)
95 | {
96 | property.Description = transformer(description);
97 | }
98 | }
99 | }
100 | }
101 | }
102 | }
103 | }
104 |
105 | private void ApplyParametersDescriptions(OpenApiOperation operation)
106 | {
107 | if (operation.Parameters is { } parameters)
108 | {
109 | foreach (var parameter in parameters)
110 | {
111 | if (parameter.Description is { } description)
112 | {
113 | parameter.Description = transformer(description);
114 | }
115 | }
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/XmlCommentsHelper.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Diagnostics;
5 | using System.Reflection;
6 |
7 | namespace MartinCostello.OpenApi;
8 |
9 | internal static class XmlCommentsHelper
10 | {
11 | public static string? GetMemberName(MethodInfo method)
12 | {
13 | if (method.DeclaringType is null)
14 | {
15 | return null;
16 | }
17 |
18 | var builder = new StringBuilder()
19 | .Append("M:")
20 | .Append(QualifiedNameFor(method.DeclaringType))
21 | .Append('.')
22 | .Append(method.Name);
23 |
24 | if (method.GetGenericArguments() is { Length: > 0 } typeParameters)
25 | {
26 | builder.Append("``")
27 | .Append(typeParameters.Length);
28 | }
29 |
30 | if (method.GetParameters() is { Length: > 0 } parameters)
31 | {
32 | string?[] names = [.. parameters.Select(GetName)];
33 |
34 | if (names.Any((p) => p is null))
35 | {
36 | return null;
37 | }
38 |
39 | builder.Append('(')
40 | .Append(string.Join(',', names))
41 | .Append(')');
42 | }
43 |
44 | return builder.ToString();
45 |
46 | static string? GetName(ParameterInfo parameter)
47 | => GetNameForType(parameter.ParameterType);
48 | }
49 |
50 | public static string? GetMemberName(MemberInfo member)
51 | {
52 | if (member.DeclaringType is null)
53 | {
54 | return null;
55 | }
56 |
57 | var builder = new StringBuilder()
58 | .Append((member.MemberType & MemberTypes.Field) != 0 ? 'F' : 'P')
59 | .Append(':')
60 | .Append(QualifiedNameFor(member.DeclaringType))
61 | .Append('.')
62 | .Append(member.Name);
63 |
64 | return builder.ToString();
65 | }
66 |
67 | private static string? QualifiedNameFor(Type type, bool expandGenericArguments = false)
68 | {
69 | if (type.IsArray)
70 | {
71 | var elementType = type.GetElementType();
72 |
73 | if (elementType is null)
74 | {
75 | return null;
76 | }
77 |
78 | return GetNameForType(elementType) + "[]";
79 | }
80 |
81 | var builder = new StringBuilder();
82 |
83 | if (!string.IsNullOrEmpty(type.Namespace))
84 | {
85 | builder.Append(type.Namespace)
86 | .Append('.');
87 | }
88 |
89 | if (type.IsNested)
90 | {
91 | builder.Append(string.Join('.', GetNestedTypeNames(type)))
92 | .Append('.');
93 | }
94 |
95 | if (type.IsConstructedGenericType && expandGenericArguments)
96 | {
97 | int index = type.Name.IndexOf('`', StringComparison.Ordinal);
98 |
99 | Debug.Assert(index is not -1, "Backtick was not found in generic type name.");
100 |
101 | builder.Append(type.Name.AsSpan(..index));
102 |
103 | var argumentNames = type
104 | .GetGenericArguments()
105 | .Select(GetNameForType);
106 |
107 | builder.Append('{')
108 | .Append(string.Join(',', argumentNames))
109 | .Append('}');
110 | }
111 | else
112 | {
113 | builder.Append(type.Name);
114 | }
115 |
116 | return builder.ToString();
117 | }
118 |
119 | private static IEnumerable GetNestedTypeNames(Type type)
120 | {
121 | if (!type.IsNested || type.DeclaringType is null)
122 | {
123 | yield break;
124 | }
125 |
126 | foreach (var name in GetNestedTypeNames(type.DeclaringType))
127 | {
128 | yield return name;
129 | }
130 |
131 | yield return type.DeclaringType.Name;
132 | }
133 |
134 | private static string? GetNameForType(Type type)
135 | {
136 | return type.IsGenericParameter ?
137 | FormattableString.Invariant($"``{type.GenericParameterPosition}") :
138 | QualifiedNameFor(type, expandGenericArguments: true);
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/OpenApi.Extensions/XmlDescriptionService.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Collections.Concurrent;
5 | using System.Reflection;
6 | using System.Xml;
7 | using System.Xml.XPath;
8 |
9 | namespace MartinCostello.OpenApi;
10 |
11 | internal sealed class XmlDescriptionService(Assembly assembly)
12 | {
13 | private readonly Assembly _assembly = assembly;
14 | private readonly ConcurrentDictionary _descriptions = [];
15 | private XPathNavigator? _navigator;
16 |
17 | public string? GetDescription(string memberName, string? parameterName = null, string? section = "summary")
18 | {
19 | var cacheKey =
20 | memberName +
21 | (!string.IsNullOrEmpty(parameterName) ? $"/{parameterName}" : $"/{section}");
22 |
23 | if (_descriptions.TryGetValue(cacheKey, out var description))
24 | {
25 | return description;
26 | }
27 |
28 | var navigator = CreateNavigator();
29 |
30 | var xmlPath =
31 | !string.IsNullOrEmpty(parameterName) ?
32 | $"/doc/members/member[@name='{memberName}']/param[@name='{parameterName}']" :
33 | $"/doc/members/member[@name='{memberName}']/{section}";
34 |
35 | if (navigator.SelectSingleNode(xmlPath) is { Value.Length: > 0 } node)
36 | {
37 | description = node.Value.Trim();
38 | }
39 |
40 | _descriptions[cacheKey] = description;
41 |
42 | return description;
43 | }
44 |
45 | private XPathNavigator CreateNavigator()
46 | {
47 | if (_navigator is null)
48 | {
49 | var path = Path.Combine(AppContext.BaseDirectory, $"{_assembly.GetName().Name}.xml");
50 | using var reader = XmlReader.Create(path);
51 | _navigator = new XPathDocument(reader).CreateNavigator();
52 | }
53 |
54 | return _navigator;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/startvs.cmd:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 | SETLOCAL
3 |
4 | :: This command launches a Visual Studio solution with environment variables required to use a local version of the .NET SDK.
5 |
6 | :: This tells .NET to use the same dotnet.exe that the build script uses.
7 | SET DOTNET_ROOT=%~dp0.dotnet
8 | SET DOTNET_ROOT(x86)=%~dp0.dotnet\x86
9 |
10 | :: Put our local dotnet.exe on PATH first so Visual Studio knows which one to use.
11 | SET PATH=%DOTNET_ROOT%;%PATH%
12 |
13 | SET sln=%~dp0OpenApi.Extensions.slnx
14 |
15 | IF NOT EXIST "%DOTNET_ROOT%\dotnet.exe" (
16 | echo The .NET SDK has not yet been installed. Run `%~dp0build.ps1` to install it
17 | exit /b 1
18 | )
19 |
20 | IF "%VSINSTALLDIR%" == "" (
21 | start "" "%sln%"
22 | ) else (
23 | "%VSINSTALLDIR%\Common7\IDE\devenv.com" "%sln%"
24 | )
25 |
--------------------------------------------------------------------------------
/startvscode.cmd:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 | SETLOCAL
3 |
4 | :: This command launches Visual Studio Code with environment variables required to use a local version of the .NET SDK.
5 |
6 | :: This tells .NET to use the same dotnet.exe that the build script uses.
7 | SET DOTNET_ROOT=%~dp0.dotnet
8 | SET DOTNET_ROOT(x86)=%~dp0.dotnet\x86
9 |
10 | :: Put our local dotnet.exe on PATH first so Visual Studio Code knows which one to use.
11 | SET PATH=%DOTNET_ROOT%;%PATH%
12 |
13 | :: Sets the Target Framework for Visual Studio Code.
14 | SET TARGET=net9.0
15 |
16 | SET FOLDER=%~1
17 |
18 | IF NOT EXIST "%DOTNET_ROOT%\dotnet.exe" (
19 | echo The .NET SDK has not yet been installed. Run `%~dp0build.ps1` to install it
20 | exit /b 1
21 | )
22 |
23 | IF "%FOLDER%"=="" (
24 | code .
25 | ) else (
26 | code "%FOLDER%"
27 | )
28 |
29 | exit /b 1
30 |
--------------------------------------------------------------------------------
/stylecop.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
3 | "settings": {
4 | "documentationRules": {
5 | "companyName": "https://github.com/martincostello/openapi-extensions",
6 | "copyrightText": "Copyright (c) {ownerName}, {year}. All rights reserved.\nLicensed under the {licenseName} license. See the {licenseFile} file in the project root for full license information.",
7 | "documentExposedElements": true,
8 | "documentInterfaces": true,
9 | "documentInternalElements": false,
10 | "documentPrivateElements": false,
11 | "documentPrivateFields": false,
12 | "fileNamingConvention": "metadata",
13 | "xmlHeader": false,
14 | "variables": {
15 | "licenseFile": "LICENSE",
16 | "licenseName": "Apache 2.0",
17 | "ownerName": "Martin Costello",
18 | "year": "2024"
19 | }
20 | },
21 | "layoutRules": {
22 | "newlineAtEndOfFile": "require"
23 | },
24 | "maintainabilityRules": {
25 | },
26 | "namingRules": {
27 | "allowCommonHungarianPrefixes": true,
28 | "allowedHungarianPrefixes": [
29 | ]
30 | },
31 | "orderingRules": {
32 | "elementOrder": [
33 | "kind",
34 | "accessibility",
35 | "constant",
36 | "static",
37 | "readonly"
38 | ],
39 | "systemUsingDirectivesFirst": true,
40 | "usingDirectivesPlacement": "outsideNamespace"
41 | },
42 | "readabilityRules": {
43 | },
44 | "spacingRules": {
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/tests/Models.A/Animal.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace Models.A;
5 |
6 | ///
7 | /// A class representing an animal.
8 | ///
9 | public class Animal : IAnimal
10 | {
11 | ///
12 | /// Gets or sets the name of the animal.
13 | ///
14 | public string? Name { get; set; }
15 | }
16 |
--------------------------------------------------------------------------------
/tests/Models.A/AnimalExampleProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using MartinCostello.OpenApi;
5 |
6 | namespace Models.A;
7 |
8 | ///
9 | /// A class representing an example provider for instances of and . This class cannot be inherited.
10 | ///
11 | public sealed class AnimalExampleProvider : IExampleProvider, IExampleProvider
12 | {
13 | ///
14 | static Animal IExampleProvider.GenerateExample() => new()
15 | {
16 | Name = "Donald",
17 | };
18 |
19 | ///
20 | static IAnimal IExampleProvider.GenerateExample() => new Animal()
21 | {
22 | Name = "Daisy",
23 | };
24 | }
25 |
--------------------------------------------------------------------------------
/tests/Models.A/AnimalsJsonSerializationContext.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Text.Json.Serialization;
5 |
6 | namespace Models.A;
7 |
8 | ///
9 | /// The to use for animals.
10 | ///
11 | [JsonSerializable(typeof(Animal))]
12 | [JsonSerializable(typeof(Animal[]))]
13 | [JsonSerializable(typeof(Cat))]
14 | [JsonSerializable(typeof(Dog))]
15 | [JsonSerializable(typeof(IAnimal))]
16 | [JsonSerializable(typeof(Spot))]
17 | [JsonSerializable(typeof(Tom))]
18 | [JsonSourceGenerationOptions(NumberHandling = JsonNumberHandling.Strict)]
19 | public sealed partial class AnimalsJsonSerializationContext : JsonSerializerContext;
20 |
--------------------------------------------------------------------------------
/tests/Models.A/Cat.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace Models.A;
5 |
6 | ///
7 | /// A class representing a cat.
8 | ///
9 | public class Cat : Animal
10 | {
11 | ///
12 | /// Gets or sets the color of the cat.
13 | ///
14 | public string? Color { get; set; }
15 | }
16 |
--------------------------------------------------------------------------------
/tests/Models.A/CatExampleProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using MartinCostello.OpenApi;
5 |
6 | namespace Models.A;
7 |
8 | ///
9 | /// A class representing an example provider for instances of . This class cannot be inherited.
10 | ///
11 | public sealed class CatExampleProvider : IExampleProvider
12 | {
13 | ///
14 | public static Cat GenerateExample() => new()
15 | {
16 | Color = "Black",
17 | Name = "Whiskers",
18 | };
19 | }
20 |
--------------------------------------------------------------------------------
/tests/Models.A/Dog.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Text.Json.Serialization;
5 | using MartinCostello.OpenApi;
6 |
7 | namespace Models.A;
8 |
9 | ///
10 | /// A class representing a dog. Secret.
11 | ///
12 | public class Dog : Animal, IExampleProvider
13 | {
14 | ///
15 | /// Gets or sets the breed of the dog.
16 | ///
17 | public string? Breed { get; set; }
18 |
19 | ///
20 | /// Gets the age of the dog, if known.
21 | ///
22 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
23 | public int? Age { get; init; }
24 |
25 | ///
26 | public static Dog GenerateExample() => new()
27 | {
28 | Breed = "Greyhound",
29 | Name = "Santa's Little Helper",
30 | };
31 | }
32 |
--------------------------------------------------------------------------------
/tests/Models.A/DogExampleProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using MartinCostello.OpenApi;
5 |
6 | namespace Models.A;
7 |
8 | ///
9 | /// A class representing an example provider for instances of . This class cannot be inherited.
10 | ///
11 | public sealed class DogExampleProvider : IExampleProvider
12 | {
13 | ///
14 | public static Dog GenerateExample() => new()
15 | {
16 | Breed = "Golden Retriever",
17 | Name = "Fido",
18 | };
19 | }
20 |
--------------------------------------------------------------------------------
/tests/Models.A/IAnimal.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using MartinCostello.OpenApi;
5 |
6 | namespace Models.A;
7 |
8 | ///
9 | /// Represents an animal.
10 | ///
11 | [OpenApiExample]
12 | public interface IAnimal
13 | {
14 | ///
15 | /// Gets or sets the name of the animal.
16 | ///
17 | string? Name { get; set; }
18 | }
19 |
--------------------------------------------------------------------------------
/tests/Models.A/Models.A.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Test models for MartinCostello.OpenApi.Extensions.
4 | true
5 | true
6 | false
7 | Library
8 | Models.A
9 | net9.0
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/Models.A/Spot.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace Models.A;
5 |
6 | ///
7 | /// A class representing Spot the dog.
8 | ///
9 | public sealed class Spot : Dog;
10 |
--------------------------------------------------------------------------------
/tests/Models.A/Tom.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace Models.A;
5 |
6 | ///
7 | /// A class representing Tom the cat.
8 | ///
9 | public sealed class Tom : Cat;
10 |
--------------------------------------------------------------------------------
/tests/Models.B/Car.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace Models.B;
5 |
6 | ///
7 | /// Represents a car.
8 | ///
9 | /// The type of the car.
10 | /// The number of wheels the vehicle has.
11 | /// The name of the manufacturer.
12 | public record Car(CarType Type, int Wheels, string Manufacturer) : Vehicle(Wheels, Manufacturer);
13 |
--------------------------------------------------------------------------------
/tests/Models.B/CarType.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace Models.B;
5 |
6 | ///
7 | /// Represents a car type.
8 | ///
9 | public enum CarType
10 | {
11 | ///
12 | /// A hatchback.
13 | ///
14 | Hatchback,
15 |
16 | ///
17 | /// A saloon.
18 | ///
19 | Saloon,
20 |
21 | ///
22 | /// An estate.
23 | ///
24 | Estate,
25 |
26 | ///
27 | /// A convertible.
28 | ///
29 | Convertible,
30 |
31 | ///
32 | /// A coupe.
33 | ///
34 | Coupe,
35 | }
36 |
--------------------------------------------------------------------------------
/tests/Models.B/Models.B.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Test models for MartinCostello.OpenApi.Extensions.
4 | true
5 | true
6 | false
7 | Library
8 | Models.B
9 | net9.0
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tests/Models.B/Motorcycle.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace Models.B;
5 |
6 | ///
7 | /// Represents a motorcycle.
8 | ///
9 | /// The name of the manufacturer.
10 | /// Whether the motorcycle has a sidecar.
11 | public record Motorcycle(string Manufacturer, bool HasSidecar = false) : Vehicle(2, Manufacturer);
12 |
--------------------------------------------------------------------------------
/tests/Models.B/Vehicle.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace Models.B;
5 |
6 | ///
7 | /// Represents a vehicle.
8 | ///
9 | /// The number of wheels the vehicle has.
10 | /// The name of the manufacturer.
11 | public record Vehicle(int Wheels, string Manufacturer);
12 |
--------------------------------------------------------------------------------
/tests/Models.B/VehiclesJsonSerializationContext.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Text.Json.Serialization;
5 |
6 | namespace Models.B;
7 |
8 | ///
9 | /// The to use for vehicles.
10 | ///
11 | [JsonSerializable(typeof(Car))]
12 | [JsonSerializable(typeof(Motorcycle))]
13 | [JsonSerializable(typeof(Vehicle))]
14 | [JsonSerializable(typeof(Vehicle[]))]
15 | [JsonSourceGenerationOptions(NumberHandling = JsonNumberHandling.Strict)]
16 | public sealed partial class VehiclesJsonSerializationContext : JsonSerializerContext;
17 |
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/AssemblyTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.OpenApi;
5 |
6 | public static class AssemblyTests
7 | {
8 | [Fact]
9 | public static void Library_Is_Strong_Named()
10 | {
11 | // Arrange
12 | var assembly = typeof(IExampleProvider<>).Assembly;
13 |
14 | // Act
15 | var name = assembly.GetName();
16 | var actual = name.GetPublicKey();
17 |
18 | // Assert
19 | actual.ShouldNotBeNull();
20 | actual.ShouldNotBeEmpty();
21 | Convert.ToHexStringLower(actual).ShouldBe("00240000048000009400000006020000002400005253413100040000010001004b0b2efbada897147aa03d2076278890aefe2f8023562336d206ec8a719b06e89461c31b43abec615918d509158629f93385930c030494509e418bf396d69ce7dbe0b5b2db1a81543ab42777cb98210677fed69dbeb3237492a7ad69e87a1911ed20eb2d7c300238dc6f6403e3d04a1351c5cb369de4e022b18fbec70f7d21ed");
22 |
23 | actual = name.GetPublicKeyToken();
24 | actual.ShouldNotBeNull();
25 | actual.ShouldNotBeEmpty();
26 | Convert.ToHexStringLower(actual).ShouldBe("9a192a7522c9e1a0");
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/ControllerTests.Schema_Is_Correct_For_Web_Api.DotNet9_0.verified.txt:
--------------------------------------------------------------------------------
1 | {
2 | openapi: 3.0.4,
3 | info: {
4 | title: WebApi | v1,
5 | version: 1.0.0
6 | },
7 | paths: {
8 | /api/Time: {
9 | get: {
10 | tags: [
11 | Time
12 | ],
13 | summary: Gets the current date and time.,
14 | description: The current date and time is returned in Coordinated Universal Time (UTC).,
15 | responses: {
16 | 200: {
17 | description: OK,
18 | content: {
19 | text/plain: {
20 | schema: {
21 | $ref: #/components/schemas/TimeModel
22 | }
23 | },
24 | application/json: {
25 | schema: {
26 | $ref: #/components/schemas/TimeModel
27 | }
28 | },
29 | text/json: {
30 | schema: {
31 | $ref: #/components/schemas/TimeModel
32 | }
33 | }
34 | }
35 | }
36 | }
37 | }
38 | }
39 | },
40 | components: {
41 | schemas: {
42 | TimeModel: {
43 | type: object,
44 | properties: {
45 | utcNow: {
46 | type: string,
47 | description: The current date and time in UTC.,
48 | format: date-time
49 | }
50 | },
51 | description: Represents the current date and time.
52 | }
53 | }
54 | },
55 | tags: [
56 | {
57 | name: Time
58 | }
59 | ]
60 | }
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/ControllerTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.OpenApi;
5 |
6 | public class ControllerTests(ITestOutputHelper outputHelper) : DocumentTests(outputHelper)
7 | {
8 | [Fact]
9 | public async Task Schema_Is_Correct_For_Web_Api()
10 | {
11 | // Arrange
12 | using var fixture = new MvcFixture(OutputHelper);
13 |
14 | // Act
15 | var actual = await fixture.GetOpenApiDocumentAsync();
16 |
17 | // Assert
18 | await VerifyJson(actual, Settings).UniqueForTargetFrameworkAndVersion();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/DocumentTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using Microsoft.AspNetCore.Routing;
5 | using Microsoft.Extensions.DependencyInjection;
6 |
7 | namespace MartinCostello.OpenApi;
8 |
9 | public abstract class DocumentTests(ITestOutputHelper outputHelper)
10 | {
11 | internal static VerifySettings Settings { get; } = CreateSettings();
12 |
13 | protected ITestOutputHelper OutputHelper { get; } = outputHelper;
14 |
15 | protected async Task VerifyOpenApiDocumentAsync(
16 | Action configureServices,
17 | Action configureEndpoints,
18 | VerifySettings? settings = null)
19 | {
20 | // Arrange
21 | using var fixture = new MinimalFixture(configureServices, configureEndpoints, OutputHelper);
22 |
23 | // Act
24 | var actual = await fixture.GetOpenApiDocumentAsync();
25 |
26 | // Assert
27 | await VerifyJson(actual, settings ?? Settings).UniqueForTargetFrameworkAndVersion();
28 | }
29 |
30 | private static VerifySettings CreateSettings()
31 | {
32 | var settings = new VerifySettings();
33 |
34 | settings.DontScrubDateTimes();
35 | settings.DontScrubGuids();
36 |
37 | return settings;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/ExampleFormatterTests.Can_Serialize_Boolean_value=False.DotNet9_0.verified.txt:
--------------------------------------------------------------------------------
1 | false
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/ExampleFormatterTests.Can_Serialize_Boolean_value=True.DotNet9_0.verified.txt:
--------------------------------------------------------------------------------
1 | true
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/ExampleFormatterTests.Can_Serialize_Complex_Object.DotNet9_0.verified.txt:
--------------------------------------------------------------------------------
1 | {
2 | Boolean: true,
3 | Integer: 42,
4 | Long: 42,
5 | String: hello world,
6 | DateTime: 2024-07-01T12:34:56Z,
7 | DateTimeOffset: 2024-07-01T12:34:56+00:00,
8 | Child: {
9 | Boolean: false,
10 | Integer: 2147483647,
11 | Long: 9.223372036854776E+18,
12 | String: nested,
13 | DateTime: 2024-06-01T12:34:56Z,
14 | DateTimeOffset: 2024-06-01T12:34:56+00:00,
15 | Child: null
16 | },
17 | Children: [
18 | {
19 | Boolean: true,
20 | Integer: -2147483648,
21 | Long: -9.223372036854776E+18,
22 | String: first,
23 | DateTime: 2024-05-01T12:34:56Z,
24 | DateTimeOffset: 2024-05-01T12:34:56+00:00,
25 | Child: null
26 | },
27 | {
28 | Boolean: false,
29 | Integer: 0,
30 | Long: 0,
31 | String: second,
32 | DateTime: 2024-04-01T12:34:56Z,
33 | DateTimeOffset: 2024-04-01T12:34:56+00:00,
34 | Child: null
35 | }
36 | ]
37 | }
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/ExampleFormatterTests.Can_Serialize_Integer.DotNet9_0.verified.txt:
--------------------------------------------------------------------------------
1 | 42
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/ExampleFormatterTests.Can_Serialize_Long.DotNet9_0.verified.txt:
--------------------------------------------------------------------------------
1 | 42
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/ExampleFormatterTests.Can_Serialize_Null_String.DotNet9_0.verified.txt:
--------------------------------------------------------------------------------
1 | null
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/ExampleFormatterTests.Can_Serialize_String.DotNet9_0.verified.txt:
--------------------------------------------------------------------------------
1 | hello world
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/ExampleFormatterTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Text.Json.Serialization;
5 | using Microsoft.OpenApi;
6 | using Microsoft.OpenApi.Writers;
7 |
8 | namespace MartinCostello.OpenApi;
9 |
10 | public static partial class ExampleFormatterTests
11 | {
12 | [Theory]
13 | [InlineData(false)]
14 | [InlineData(true)]
15 | public static async Task Can_Serialize_Boolean(bool value)
16 | {
17 | // Act
18 | var actual = Serialize(value);
19 |
20 | // Assert
21 | await VerifyJson(actual).UseParameters(value).UniqueForTargetFrameworkAndVersion();
22 | }
23 |
24 | [Fact]
25 | public static async Task Can_Serialize_Integer()
26 | {
27 | // Arrange
28 | int value = 42;
29 |
30 | // Act
31 | var actual = Serialize(value);
32 |
33 | // Assert
34 | await VerifyJson(actual).UniqueForTargetFrameworkAndVersion();
35 | }
36 |
37 | [Fact]
38 | public static async Task Can_Serialize_Long()
39 | {
40 | // Arrange
41 | long value = 42;
42 |
43 | // Act
44 | var actual = Serialize(value);
45 |
46 | // Assert
47 | await VerifyJson(actual).UniqueForTargetFrameworkAndVersion();
48 | }
49 |
50 | [Fact]
51 | public static async Task Can_Serialize_String()
52 | {
53 | // Arrange
54 | string value = "hello world";
55 |
56 | // Act
57 | var actual = Serialize(value);
58 |
59 | // Assert
60 | await VerifyJson(actual).UniqueForTargetFrameworkAndVersion();
61 | }
62 |
63 | [Fact]
64 | public static async Task Can_Serialize_Null_String()
65 | {
66 | // Arrange
67 | string? value = null;
68 |
69 | // Act
70 | var actual = Serialize(value);
71 |
72 | // Assert
73 | await VerifyJson(actual).UniqueForTargetFrameworkAndVersion();
74 | }
75 |
76 | [Fact]
77 | public static async Task Can_Serialize_Complex_Object()
78 | {
79 | // Arrange
80 | var value = new Custom()
81 | {
82 | Boolean = true,
83 | DateTime = new DateTime(2024, 7, 1, 12, 34, 56, DateTimeKind.Utc),
84 | DateTimeOffset = new DateTimeOffset(2024, 7, 1, 12, 34, 56, TimeSpan.Zero),
85 | Integer = 42,
86 | Long = 42,
87 | String = "hello world",
88 | Child = new Custom()
89 | {
90 | Boolean = false,
91 | DateTime = new DateTime(2024, 6, 1, 12, 34, 56, DateTimeKind.Utc),
92 | DateTimeOffset = new DateTimeOffset(2024, 6, 1, 12, 34, 56, TimeSpan.Zero),
93 | Integer = int.MaxValue,
94 | Long = long.MaxValue,
95 | String = "nested",
96 | },
97 | Children =
98 | [
99 | new()
100 | {
101 | Boolean = true,
102 | DateTime = new DateTime(2024, 5, 1, 12, 34, 56, DateTimeKind.Utc),
103 | DateTimeOffset = new DateTimeOffset(2024, 5, 1, 12, 34, 56, TimeSpan.Zero),
104 | Integer = int.MinValue,
105 | Long = long.MinValue,
106 | String = "first",
107 | },
108 | new()
109 | {
110 | Boolean = false,
111 | DateTime = new DateTime(2024, 4, 1, 12, 34, 56, DateTimeKind.Utc),
112 | DateTimeOffset = new DateTimeOffset(2024, 4, 1, 12, 34, 56, TimeSpan.Zero),
113 | Integer = 0,
114 | Long = 0,
115 | String = "second",
116 | },
117 | ],
118 | };
119 |
120 | // Act
121 | var actual = Serialize(value);
122 |
123 | // Assert
124 | await VerifyJson(actual, DocumentTests.Settings).UniqueForTargetFrameworkAndVersion();
125 | }
126 |
127 | private static string? Serialize(T value)
128 | {
129 | var actual = ExampleFormatter.AsJson(value, ReflectionJsonSerializerContext.Default);
130 |
131 | using var stringWriter = new StringWriter();
132 | var jsonWriter = new OpenApiJsonWriter(stringWriter);
133 |
134 | actual.Write(jsonWriter, OpenApiSpecVersion.OpenApi3_0);
135 |
136 | return stringWriter.ToString();
137 | }
138 |
139 | private sealed class Custom
140 | {
141 | public bool Boolean { get; set; }
142 |
143 | public int Integer { get; set; }
144 |
145 | public long Long { get; set; }
146 |
147 | public string String { get; set; } = default!;
148 |
149 | public DateTime DateTime { get; set; }
150 |
151 | public DateTimeOffset DateTimeOffset { get; set; }
152 |
153 | public Custom Child { get; set; } = default!;
154 |
155 | public IList Children { get; set; } = [];
156 | }
157 |
158 | [JsonSerializable(typeof(bool))]
159 | [JsonSerializable(typeof(int))]
160 | [JsonSerializable(typeof(long))]
161 | [JsonSerializable(typeof(string))]
162 | [JsonSerializable(typeof(DateTime))]
163 | [JsonSerializable(typeof(DateTimeOffset))]
164 | [JsonSerializable(typeof(Custom))]
165 | [JsonSourceGenerationOptions(NumberHandling = JsonNumberHandling.Strict)]
166 | private sealed partial class ReflectionJsonSerializerContext : JsonSerializerContext;
167 | }
168 |
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/IWebHostBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using Microsoft.AspNetCore.Hosting;
5 | using Microsoft.Extensions.Logging;
6 |
7 | namespace MartinCostello.OpenApi;
8 |
9 | internal static class IWebHostBuilderExtensions
10 | {
11 | public static IWebHostBuilder ConfigureXUnitLogging(this IWebHostBuilder builder, ITestOutputHelper outputHelper)
12 | {
13 | return builder.ConfigureLogging((builder) =>
14 | {
15 | builder.ClearProviders()
16 | .AddXUnit(outputHelper)
17 | .SetMinimumLevel(LogLevel.Information);
18 | });
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/IntegrationTests.Http_404_Is_Returned_If_Yaml_Document_Not_Found.verified.txt:
--------------------------------------------------------------------------------
1 | No OpenAPI document with the name 'does-not-exist' was found.
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/IntegrationTests.Schema_Is_Correct_For_App.DotNet9_0.verified.txt:
--------------------------------------------------------------------------------
1 | {
2 | openapi: 3.0.4,
3 | info: {
4 | title: TestApp | v1,
5 | version: 1.0.0
6 | },
7 | servers: [
8 | {
9 | url: http://localhost
10 | }
11 | ],
12 | paths: {
13 | /hello: {
14 | get: {
15 | tags: [
16 | TestApp
17 | ],
18 | parameters: [
19 | {
20 | name: name,
21 | in: query,
22 | description: The name of the person to greet.,
23 | schema: {
24 | type: string
25 | },
26 | example: Martin
27 | }
28 | ],
29 | responses: {
30 | 200: {
31 | description: A greeting.,
32 | content: {
33 | application/json: {
34 | schema: {
35 | $ref: #/components/schemas/Greeting
36 | },
37 | example: {
38 | text: Hello, World!
39 | }
40 | }
41 | }
42 | },
43 | 400: {
44 | description: No name was provided.,
45 | content: {
46 | application/problem+json: {
47 | schema: {
48 | $ref: #/components/schemas/ProblemDetails
49 | },
50 | example: {
51 | type: https://tools.ietf.org/html/rfc9110#section-15.6.1,
52 | title: Internal Server Error,
53 | status: 500,
54 | detail: An internal error occurred.,
55 | instance: /hello
56 | }
57 | }
58 | }
59 | }
60 | }
61 | }
62 | }
63 | },
64 | components: {
65 | schemas: {
66 | Greeting: {
67 | type: object,
68 | properties: {
69 | text: {
70 | type: string,
71 | description: The text of the greeting.,
72 | nullable: true
73 | }
74 | },
75 | description: Represents a greeting.,
76 | example: {
77 | text: Hello, World!
78 | }
79 | },
80 | ProblemDetails: {
81 | type: object,
82 | properties: {
83 | type: {
84 | type: string,
85 | nullable: true
86 | },
87 | title: {
88 | type: string,
89 | nullable: true
90 | },
91 | status: {
92 | type: integer,
93 | format: int32,
94 | nullable: true
95 | },
96 | detail: {
97 | type: string,
98 | nullable: true
99 | },
100 | instance: {
101 | type: string,
102 | nullable: true
103 | }
104 | },
105 | example: {
106 | type: https://tools.ietf.org/html/rfc9110#section-15.6.1,
107 | title: Internal Server Error,
108 | status: 500,
109 | detail: An internal error occurred.,
110 | instance: /hello
111 | }
112 | }
113 | }
114 | },
115 | tags: [
116 | {
117 | name: TestApp
118 | }
119 | ]
120 | }
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/IntegrationTests.Schema_Is_Correct_For_App_As_Yaml.DotNet9_0.verified.txt:
--------------------------------------------------------------------------------
1 | openapi: 3.0.4
2 | info:
3 | title: TestApp | v1
4 | version: 1.0.0
5 | servers:
6 | - url: http://localhost
7 | paths:
8 | /hello:
9 | get:
10 | tags:
11 | - TestApp
12 | parameters:
13 | - name: name
14 | in: query
15 | description: The name of the person to greet.
16 | schema:
17 | type: string
18 | example: Martin
19 | responses:
20 | '200':
21 | description: A greeting.
22 | content:
23 | application/json:
24 | schema:
25 | $ref: '#/components/schemas/Greeting'
26 | example:
27 | text: 'Hello, World!'
28 | '400':
29 | description: No name was provided.
30 | content:
31 | application/problem+json:
32 | schema:
33 | $ref: '#/components/schemas/ProblemDetails'
34 | example:
35 | type: https://tools.ietf.org/html/rfc9110#section-15.6.1
36 | title: Internal Server Error
37 | status: 500
38 | detail: An internal error occurred.
39 | instance: /hello
40 | components:
41 | schemas:
42 | Greeting:
43 | type: object
44 | properties:
45 | text:
46 | type: string
47 | description: The text of the greeting.
48 | nullable: true
49 | description: Represents a greeting.
50 | example:
51 | text: 'Hello, World!'
52 | ProblemDetails:
53 | type: object
54 | properties:
55 | type:
56 | type: string
57 | nullable: true
58 | title:
59 | type: string
60 | nullable: true
61 | status:
62 | type: integer
63 | format: int32
64 | nullable: true
65 | detail:
66 | type: string
67 | nullable: true
68 | instance:
69 | type: string
70 | nullable: true
71 | example:
72 | type: https://tools.ietf.org/html/rfc9110#section-15.6.1
73 | title: Internal Server Error
74 | status: 500
75 | detail: An internal error occurred.
76 | instance: /hello
77 | tags:
78 | - name: TestApp
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/IntegrationTests.Schema_Is_Correct_For_Classes.DotNet9_0.verified.txt:
--------------------------------------------------------------------------------
1 | {
2 | openapi: 3.0.4,
3 | info: {
4 | title: TestApp | v1,
5 | version: 1.0.0
6 | },
7 | paths: {
8 | /register: {
9 | post: {
10 | tags: [
11 | TestApp
12 | ],
13 | requestBody: {
14 | content: {
15 | application/json: {
16 | schema: {
17 | $ref: #/components/schemas/Animal
18 | },
19 | example: {
20 | Name: Donald
21 | }
22 | }
23 | },
24 | required: true
25 | },
26 | responses: {
27 | 201: {
28 | description: Created
29 | }
30 | }
31 | }
32 | },
33 | /adopt/cat: {
34 | post: {
35 | tags: [
36 | TestApp
37 | ],
38 | requestBody: {
39 | content: {
40 | application/json: {
41 | schema: {
42 | $ref: #/components/schemas/Cat
43 | },
44 | example: {
45 | Name: Donald
46 | }
47 | }
48 | },
49 | required: true
50 | },
51 | responses: {
52 | 204: {
53 | description: No Content
54 | }
55 | }
56 | }
57 | },
58 | /adopt/dog: {
59 | post: {
60 | tags: [
61 | TestApp
62 | ],
63 | requestBody: {
64 | content: {
65 | application/json: {
66 | schema: {
67 | $ref: #/components/schemas/Dog
68 | },
69 | example: {
70 | Breed: Greyhound,
71 | Name: Santa's Little Helper
72 | }
73 | }
74 | },
75 | required: true
76 | },
77 | responses: {
78 | 204: {
79 | description: No Content
80 | }
81 | }
82 | }
83 | },
84 | /animals: {
85 | get: {
86 | tags: [
87 | TestApp
88 | ],
89 | responses: {
90 | 200: {
91 | description: OK,
92 | content: {
93 | application/json: {
94 | schema: {
95 | type: array,
96 | items: {
97 | $ref: #/components/schemas/Animal
98 | }
99 | }
100 | }
101 | }
102 | }
103 | }
104 | }
105 | }
106 | },
107 | components: {
108 | schemas: {
109 | Animal: {
110 | type: object,
111 | properties: {
112 | name: {
113 | type: string,
114 | description: The name of the animal.,
115 | nullable: true
116 | }
117 | },
118 | description: A class representing an animal.,
119 | example: {
120 | Name: Donald
121 | }
122 | },
123 | Cat: {
124 | type: object,
125 | properties: {
126 | color: {
127 | type: string,
128 | description: The color of the cat.,
129 | nullable: true
130 | },
131 | name: {
132 | type: string,
133 | description: The name of the animal.,
134 | nullable: true
135 | }
136 | },
137 | description: A class representing a cat.,
138 | example: {
139 | Name: Donald
140 | }
141 | },
142 | Dog: {
143 | type: object,
144 | properties: {
145 | breed: {
146 | type: string,
147 | description: The breed of the dog.,
148 | nullable: true
149 | },
150 | age: {
151 | type: integer,
152 | description: The age of the dog, if known.,
153 | format: int32,
154 | nullable: true
155 | },
156 | name: {
157 | type: string,
158 | description: The name of the animal.,
159 | nullable: true
160 | }
161 | },
162 | description: A class representing a dog.,
163 | example: {
164 | Breed: Greyhound,
165 | Name: Santa's Little Helper
166 | }
167 | }
168 | }
169 | },
170 | tags: [
171 | {
172 | name: TestApp
173 | }
174 | ]
175 | }
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/IntegrationTests.Schema_Is_Correct_For_Interfaces.DotNet9_0.verified.txt:
--------------------------------------------------------------------------------
1 | {
2 | openapi: 3.0.4,
3 | info: {
4 | title: TestApp | v1,
5 | version: 1.0.0
6 | },
7 | paths: {
8 | /register: {
9 | post: {
10 | tags: [
11 | TestApp
12 | ],
13 | requestBody: {
14 | content: {
15 | application/json: {
16 | schema: {
17 | $ref: #/components/schemas/IAnimal
18 | },
19 | example: {
20 | Name: Daisy
21 | }
22 | }
23 | },
24 | required: true
25 | },
26 | responses: {
27 | 201: {
28 | description: Created
29 | }
30 | }
31 | }
32 | }
33 | },
34 | components: {
35 | schemas: {
36 | IAnimal: {
37 | type: object,
38 | properties: {
39 | name: {
40 | type: string,
41 | description: The name of the animal.,
42 | nullable: true
43 | }
44 | },
45 | description: Represents an animal.,
46 | example: {
47 | Name: Daisy
48 | }
49 | }
50 | }
51 | },
52 | tags: [
53 | {
54 | name: TestApp
55 | }
56 | ]
57 | }
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/IntegrationTests.Schema_Is_Correct_For_Not_Json.DotNet9_0.verified.txt:
--------------------------------------------------------------------------------
1 | {
2 | openapi: 3.0.4,
3 | info: {
4 | title: TestApp | v1,
5 | version: 1.0.0
6 | },
7 | servers: [
8 | {
9 | url: http://localhost
10 | }
11 | ],
12 | paths: {
13 | /greet: {
14 | get: {
15 | tags: [
16 | IntegrationTests
17 | ],
18 | responses: {
19 | 200: {
20 | description: OK,
21 | content: {
22 | text/plain: {
23 | schema: {
24 | type: string
25 | },
26 | example: Bonjour!
27 | }
28 | }
29 | }
30 | }
31 | }
32 | }
33 | },
34 | tags: [
35 | {
36 | name: IntegrationTests
37 | }
38 | ]
39 | }
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/IntegrationTests.Schema_Is_Correct_For_Records.DotNet9_0.verified.txt:
--------------------------------------------------------------------------------
1 | {
2 | openapi: 3.0.4,
3 | info: {
4 | title: TestApp | v1,
5 | version: 1.0.0
6 | },
7 | paths: {
8 | /car: {
9 | post: {
10 | tags: [
11 | TestApp
12 | ],
13 | requestBody: {
14 | content: {
15 | application/json: {
16 | schema: {
17 | $ref: #/components/schemas/Car
18 | }
19 | }
20 | },
21 | required: true
22 | },
23 | responses: {
24 | 204: {
25 | description: No Content
26 | }
27 | }
28 | }
29 | },
30 | /vehicles: {
31 | get: {
32 | tags: [
33 | TestApp
34 | ],
35 | responses: {
36 | 200: {
37 | description: OK,
38 | content: {
39 | application/json: {
40 | schema: {
41 | type: array,
42 | items: {
43 | $ref: #/components/schemas/Vehicle
44 | }
45 | }
46 | }
47 | }
48 | }
49 | }
50 | }
51 | }
52 | },
53 | components: {
54 | schemas: {
55 | Car: {
56 | required: [
57 | type,
58 | wheels,
59 | manufacturer
60 | ],
61 | type: object,
62 | properties: {
63 | type: {
64 | $ref: #/components/schemas/CarType
65 | },
66 | wheels: {
67 | type: integer,
68 | description: The number of wheels the vehicle has.,
69 | format: int32
70 | },
71 | manufacturer: {
72 | type: string,
73 | description: The name of the manufacturer.
74 | }
75 | },
76 | description: Represents a car.
77 | },
78 | CarType: {
79 | type: integer,
80 | description: The type of the car.
81 | },
82 | Vehicle: {
83 | required: [
84 | wheels,
85 | manufacturer
86 | ],
87 | type: object,
88 | properties: {
89 | wheels: {
90 | type: integer,
91 | description: The number of wheels the vehicle has.,
92 | format: int32
93 | },
94 | manufacturer: {
95 | type: string,
96 | description: The name of the manufacturer.
97 | }
98 | },
99 | description: Represents a vehicle.
100 | }
101 | }
102 | },
103 | tags: [
104 | {
105 | name: TestApp
106 | }
107 | ]
108 | }
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/MartinCostello.OpenApi.Extensions.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Tests for MartinCostello.OpenApi.Extensions.
4 | true
5 | false
6 | false
7 | Exe
8 | MartinCostello.OpenApi
9 | net9.0
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | true
34 | 85
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/MinimalFixture.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using Microsoft.AspNetCore.Builder;
5 | using Microsoft.AspNetCore.Hosting;
6 | using Microsoft.AspNetCore.Mvc.Testing;
7 | using Microsoft.AspNetCore.Routing;
8 | using Microsoft.Extensions.DependencyInjection;
9 |
10 | namespace MartinCostello.OpenApi;
11 |
12 | public sealed class MinimalFixture(
13 | Action configureServices,
14 | Action configureEndpoints,
15 | ITestOutputHelper outputHelper) : WebApplicationFactory()
16 | {
17 | protected override void ConfigureWebHost(IWebHostBuilder builder)
18 | {
19 | builder.ConfigureXUnitLogging(outputHelper);
20 | builder.ConfigureServices((services) =>
21 | {
22 | services.AddSingleton(new ConfigureEndpointsFilter(configureEndpoints));
23 | configureServices(services);
24 | });
25 | }
26 |
27 | private sealed class ConfigureEndpointsFilter(Action configure) : IStartupFilter
28 | {
29 | public Action Configure(Action next)
30 | {
31 | return (builder) =>
32 | {
33 | next(builder);
34 | builder.UseEndpoints(configure);
35 | };
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/MvcFixture.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using MartinCostello.WebApi.Models;
5 | using Microsoft.AspNetCore.Hosting;
6 | using Microsoft.AspNetCore.Mvc.Testing;
7 |
8 | namespace MartinCostello.OpenApi;
9 |
10 | public sealed class MvcFixture(ITestOutputHelper outputHelper) : WebApplicationFactory()
11 | {
12 | protected override void ConfigureWebHost(IWebHostBuilder builder)
13 | => builder.ConfigureXUnitLogging(outputHelper);
14 | }
15 |
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/OpenApiEndpointConventionBuilderExtensionsTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using Microsoft.AspNetCore.Builder;
5 |
6 | namespace MartinCostello.OpenApi;
7 |
8 | public static class OpenApiEndpointConventionBuilderExtensionsTests
9 | {
10 | [Fact]
11 | public static void ProducesOpenApiResponse_Throws_If_Builder_Is_Null()
12 | {
13 | // Arrange
14 | IEndpointConventionBuilder builder = null!;
15 |
16 | // Act and Assert
17 | Should.Throw(() => builder.ProducesOpenApiResponse(200, "OK"));
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/OpenApiExampleAttributeTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Text.Json;
5 | using System.Text.Json.Serialization;
6 | using Microsoft.OpenApi.Writers;
7 |
8 | namespace MartinCostello.OpenApi;
9 |
10 | public static partial class OpenApiExampleAttributeTests
11 | {
12 | [Fact]
13 | public static void OpenApiExampleAttribute_Constructor_Initializes_Instance()
14 | {
15 | // Arrange
16 | var value = "example";
17 |
18 | // Act
19 | var actual = new OpenApiExampleAttribute(value);
20 |
21 | // Assert
22 | actual.Value.ShouldBe(value);
23 | actual.GenerateExample().ShouldBe(value);
24 | }
25 |
26 | [Fact]
27 | public static void OpenApiExampleAttribute_IExampleProvider_Implementation_Is_Correct()
28 | {
29 | // Act
30 | var actual = GetExample();
31 |
32 | // Assert
33 | actual.ShouldBe("value");
34 | }
35 |
36 | [Fact]
37 | public static void OpenApiExampleAttributeT1_Constructor_Initializes_Instance()
38 | {
39 | // Act
40 | var actual = new OpenApiExampleAttribute();
41 |
42 | // Assert
43 | actual.ExampleType.ShouldBe(typeof(Human));
44 | }
45 |
46 | [Fact]
47 | public static void OpenApiExampleAttributeT1_GenerateExample_Returns_Correct_Value()
48 | {
49 | // Arrange
50 | var target = new OpenApiExampleAttribute();
51 |
52 | // Act
53 | var actual = target.GenerateExample();
54 |
55 | // Assert
56 | actual.ShouldNotBeNull();
57 | actual.Name.ShouldBe("Martin");
58 | }
59 |
60 | [Fact]
61 | public static void OpenApiExampleAttributeT2_Constructor_Initializes_Instance()
62 | {
63 | // Act
64 | var actual = new OpenApiExampleAttribute();
65 |
66 | // Assert
67 | actual.ExampleType.ShouldBe(typeof(Person));
68 | }
69 |
70 | [Fact]
71 | public static void OpenApiExampleAttributeT2_GenerateExample_Returns_Correct_Value()
72 | {
73 | // Arrange
74 | var target = new OpenApiExampleAttribute();
75 |
76 | // Act
77 | var actual = target.GenerateExample();
78 |
79 | // Assert
80 | actual.ShouldNotBeNull();
81 | actual.Name.ShouldBe("Martin");
82 | }
83 |
84 | [Theory]
85 | [InlineData(false, "Name")]
86 | [InlineData(true, "name")]
87 | public static void OpenApiExampleAttributeT2_GenerateExample_As_IOpenApiExampleMetadata_Returns_Correct_Value(
88 | bool camelCase,
89 | string expectedPropertyName)
90 | {
91 | // Arrange
92 | IOpenApiExampleMetadata target = new OpenApiExampleAttribute();
93 | JsonSerializerContext context = camelCase ? PersonJsonSerializationContextCamel.Default : PersonJsonSerializationContextPascal.Default;
94 |
95 | // Act
96 | var actual = target.GenerateExample(context);
97 |
98 | // Assert
99 | actual.ShouldNotBeNull();
100 |
101 | // Arrange
102 | using var stringWriter = new StringWriter();
103 | var jsonWriter = new OpenApiJsonWriter(stringWriter);
104 |
105 | // Act
106 | actual.Write(jsonWriter, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0);
107 |
108 | // Assert
109 | using var document = JsonDocument.Parse(stringWriter.ToString());
110 |
111 | document.ShouldNotBeNull();
112 | document.RootElement.ValueKind.ShouldBe(JsonValueKind.Object);
113 | document.RootElement.EnumerateObject().Count().ShouldBe(1);
114 | document.RootElement.TryGetProperty(expectedPropertyName, out var value).ShouldBeTrue();
115 |
116 | value.ValueKind.ShouldBe(JsonValueKind.String);
117 | value.GetString().ShouldBe("Martin");
118 | }
119 |
120 | private static TSchema GetExample()
121 | where TProvider : IExampleProvider
122 | => TProvider.GenerateExample();
123 |
124 | private sealed record Human(string Name) : IExampleProvider
125 | {
126 | public static Human GenerateExample() => new("Martin");
127 | }
128 |
129 | private sealed record Person(string Name);
130 |
131 | private sealed class PersonExampleProvider : IExampleProvider
132 | {
133 | public static Person GenerateExample() => new("Martin");
134 | }
135 |
136 | [JsonSerializable(typeof(Person))]
137 | [JsonSourceGenerationOptions(
138 | NumberHandling = JsonNumberHandling.Strict,
139 | PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
140 | private sealed partial class PersonJsonSerializationContextCamel : JsonSerializerContext;
141 |
142 | [JsonSerializable(typeof(Person))]
143 | [JsonSourceGenerationOptions(
144 | NumberHandling = JsonNumberHandling.Strict,
145 | PropertyNamingPolicy = JsonKnownNamingPolicy.Unspecified)]
146 | private sealed partial class PersonJsonSerializationContextPascal : JsonSerializerContext;
147 | }
148 |
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/OpenApiExtensionsTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using Microsoft.AspNetCore.OpenApi;
5 | using Microsoft.Extensions.DependencyInjection;
6 | using Microsoft.Extensions.Options;
7 |
8 | namespace MartinCostello.OpenApi;
9 |
10 | public static class OpenApiExtensionsTests
11 | {
12 | [Fact]
13 | public static void AddOpenApiExtensions_Validates_Arguments()
14 | {
15 | // Arrange
16 | string documentName = "api";
17 |
18 | IServiceCollection services = null!;
19 |
20 | // Act and Assert
21 | Should.Throw(services.AddOpenApiExtensions).ParamName.ShouldBe("services");
22 | Should.Throw(() => services.AddOpenApiExtensions(documentName)).ParamName.ShouldBe("services");
23 | Should.Throw(() => services.AddOpenApiExtensions(documentName, ConfigureOneService)).ParamName.ShouldBe("services");
24 | Should.Throw(() => services.AddOpenApiExtensions(documentName, ConfigureTwoServices)).ParamName.ShouldBe("services");
25 | Should.Throw(() => services.AddOpenApiExtensions(ConfigureOneService)).ParamName.ShouldBe("services");
26 | Should.Throw(() => services.AddOpenApiExtensions(ConfigureTwoServices)).ParamName.ShouldBe("services");
27 |
28 | // Arrange
29 | services = new ServiceCollection();
30 | Action configureOne = null!;
31 | Action configureTwo = null!;
32 |
33 | // Act and Assert
34 | Should.Throw(() => services.AddOpenApiExtensions(configureOne)).ParamName.ShouldBe("configureOptions");
35 | Should.Throw(() => services.AddOpenApiExtensions(configureTwo)).ParamName.ShouldBe("configureOptions");
36 | Should.Throw(() => services.AddOpenApiExtensions(documentName, configureOne)).ParamName.ShouldBe("configureOptions");
37 | Should.Throw(() => services.AddOpenApiExtensions(documentName, configureTwo)).ParamName.ShouldBe("configureOptions");
38 |
39 | static void ConfigureOneService(OpenApiExtensionsOptions options)
40 | => options.ShouldNotBeNull();
41 |
42 | static void ConfigureTwoServices(OpenApiOptions first, OpenApiExtensionsOptions second)
43 | {
44 | first.ShouldNotBeNull();
45 | second.ShouldNotBeNull();
46 | }
47 | }
48 |
49 | [Fact]
50 | public static void AddOpenApiExtensions_Throws_If_Examples_Enabled_But_Not_JsonSerializerContext_Specified()
51 | {
52 | // Arrange
53 | var services = new ServiceCollection();
54 |
55 | services.AddOpenApi();
56 | services.AddOpenApiExtensions((options) =>
57 | {
58 | options.AddExamples = true;
59 | options.SerializationContexts.Clear();
60 | });
61 |
62 | using var serviceProvider = services.BuildServiceProvider();
63 | var monitor = serviceProvider.GetRequiredService>();
64 |
65 | // Act and Assert
66 | var exception = Should.Throw(() => monitor.Get("v1"));
67 | exception.Message.ShouldBe(@"No JsonSerializerContext has been configured on the OpenApiExtensionsOptions instance for the OpenAPI document ""v1"".");
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/OpenApiResponseAttributeTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.OpenApi;
5 |
6 | public static class OpenApiResponseAttributeTests
7 | {
8 | [Fact]
9 | public static void OpenApiResponseAttribute_Constructor_Initializes_Instance()
10 | {
11 | // Arrange
12 | var statusCode = 200;
13 | var description = "OK";
14 |
15 | // Act
16 | var actual = new OpenApiResponseAttribute(statusCode, description);
17 |
18 | // Assert
19 | actual.HttpStatusCode.ShouldBe(statusCode);
20 | actual.Description.ShouldBe(description);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/OpenApi.Extensions.Tests/WebApplicationFactoryExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using Microsoft.AspNetCore.Mvc.Testing;
5 |
6 | namespace MartinCostello.OpenApi;
7 |
8 | internal static class WebApplicationFactoryExtensions
9 | {
10 | public static async Task GetOpenApiDocumentAsync(this WebApplicationFactory fixture)
11 | where T : class
12 | {
13 | using var client = fixture.CreateDefaultClient();
14 | return await client.GetStringAsync("/openapi/v1.json");
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/TestApp/AppJsonSerializationContext.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Text.Json.Serialization;
5 | using Microsoft.AspNetCore.Mvc;
6 |
7 | namespace MartinCostello.OpenApi;
8 |
9 | ///
10 | /// The to use for the application. This class cannot be inherited.
11 | ///
12 | [JsonSerializable(typeof(Greeting))]
13 | [JsonSerializable(typeof(ProblemDetails))]
14 | [JsonSerializable(typeof(string))]
15 | [JsonSourceGenerationOptions(
16 | NumberHandling = JsonNumberHandling.Strict,
17 | PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
18 | WriteIndented = true)]
19 | public sealed partial class AppJsonSerializationContext : JsonSerializerContext;
20 |
--------------------------------------------------------------------------------
/tests/TestApp/Greeting.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.OpenApi;
5 |
6 | ///
7 | /// Represents a greeting.
8 | ///
9 | [OpenApiExample]
10 | public sealed class Greeting : IExampleProvider
11 | {
12 | ///
13 | /// Gets or sets the text of the greeting.
14 | ///
15 | public string? Text { get; set; }
16 |
17 | ///
18 | public static Greeting GenerateExample()
19 | {
20 | return new() { Text = "Hello, World!" };
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/TestApp/ProblemDetailsExampleProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using Microsoft.AspNetCore.Mvc;
5 |
6 | namespace MartinCostello.OpenApi;
7 |
8 | ///
9 | /// A class representing an example provider for .
10 | ///
11 | public sealed class ProblemDetailsExampleProvider : IExampleProvider
12 | {
13 | ///
14 | public static ProblemDetails GenerateExample()
15 | {
16 | return new()
17 | {
18 | Type = "https://tools.ietf.org/html/rfc9110#section-15.6.1",
19 | Title = "Internal Server Error",
20 | Status = StatusCodes.Status500InternalServerError,
21 | Detail = "An internal error occurred.",
22 | Instance = "/hello",
23 | };
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/TestApp/Program.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using Microsoft.AspNetCore.WebUtilities;
5 |
6 | var builder = WebApplication.CreateBuilder(args);
7 |
8 | builder.Services.AddProblemDetails();
9 | builder.Services.Configure((options) =>
10 | {
11 | options.CustomizeProblemDetails = (context) =>
12 | {
13 | if (context.Exception is not null)
14 | {
15 | context.ProblemDetails.Detail = "An internal error occurred.";
16 | }
17 |
18 | context.ProblemDetails.Instance = context.HttpContext.Request.Path;
19 | context.ProblemDetails.Title = ReasonPhrases.GetReasonPhrase(context.ProblemDetails.Status ?? StatusCodes.Status500InternalServerError);
20 | };
21 | });
22 |
23 | var app = builder.Build();
24 |
25 | app.MapOpenApi();
26 |
27 | app.Run();
28 |
29 | namespace MartinCostello.OpenApi
30 | {
31 | public partial class Program
32 | {
33 | // Expose the Program class for use with WebApplicationFactory
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tests/TestApp/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "TestApp": {
4 | "commandName": "Project",
5 | "launchBrowser": true,
6 | "environmentVariables": {
7 | "ASPNETCORE_ENVIRONMENT": "Development"
8 | },
9 | "applicationUrl": "https://localhost:55219;http://localhost:55220"
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tests/TestApp/TestApp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Test application.
4 | true
5 | MartinCostello.OpenApi
6 | net9.0
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/WebApi/Controllers/TimeController.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using MartinCostello.WebApi.Models;
5 | using Microsoft.AspNetCore.Mvc;
6 |
7 | namespace MartinCostello.WebApi.Controllers;
8 |
9 | ///
10 | /// Represents the controller for the time API.
11 | ///
12 | [ApiController]
13 | [Route("api/[controller]")]
14 | public class TimeController(TimeProvider timeProvider) : ControllerBase
15 | {
16 | ///
17 | /// Gets the current date and time.
18 | ///
19 | ///
20 | /// The current date and time.
21 | ///
22 | ///
23 | /// The current date and time is returned in Coordinated Universal Time (UTC).
24 | ///
25 | ///
26 | /// {"utcNow":"2025-03-13T10:18:37.8147935+00:00"}
27 | ///
28 | [HttpGet]
29 | public ActionResult Now()
30 | => Ok(new TimeModel { UtcNow = timeProvider.GetUtcNow() });
31 | }
32 |
--------------------------------------------------------------------------------
/tests/WebApi/Models/TimeModel.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.WebApi.Models;
5 |
6 | ///
7 | /// Represents the current date and time.
8 | ///
9 | public class TimeModel
10 | {
11 | ///
12 | /// Gets or sets the current date and time in UTC.
13 | ///
14 | public DateTimeOffset UtcNow { get; set; }
15 | }
16 |
--------------------------------------------------------------------------------
/tests/WebApi/MvcSerializerContext.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Text.Json.Serialization;
5 | using MartinCostello.WebApi.Models;
6 |
7 | namespace MartinCostello.WebApi;
8 |
9 | [JsonSerializable(typeof(DateTimeOffset))]
10 | [JsonSerializable(typeof(TimeModel))]
11 | [JsonSourceGenerationOptions(
12 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
13 | NumberHandling = JsonNumberHandling.Strict,
14 | PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
15 | WriteIndented = true)]
16 | public sealed partial class MvcSerializerContext : JsonSerializerContext;
17 |
--------------------------------------------------------------------------------
/tests/WebApi/Program.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2024. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Text.Json;
5 | using MartinCostello.OpenApi;
6 | using MartinCostello.WebApi;
7 | using MartinCostello.WebApi.Models;
8 |
9 | var builder = WebApplication.CreateBuilder(args);
10 |
11 | builder.Services.AddSingleton(TimeProvider.System);
12 |
13 | builder.Services.AddControllers().AddJsonOptions((p) => ConfigureJsonSerialization(p.JsonSerializerOptions));
14 | builder.Services.ConfigureHttpJsonOptions((p) => ConfigureJsonSerialization(p.SerializerOptions));
15 |
16 | builder.Services.AddOpenApi();
17 | builder.Services.AddOpenApiExtensions((p) => p.AddXmlComments());
18 |
19 | var app = builder.Build();
20 |
21 | app.UseRouting();
22 |
23 | app.MapControllerRoute(
24 | name: "default",
25 | pattern: "{controller=Time}/{action=Now}/{id?}");
26 |
27 | app.MapOpenApi();
28 |
29 | app.Run();
30 |
31 | static void ConfigureJsonSerialization(JsonSerializerOptions options)
32 | => options.TypeInfoResolverChain.Add(MvcSerializerContext.Default);
33 |
--------------------------------------------------------------------------------
/tests/WebApi/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "WebApi": {
5 | "commandName": "Project",
6 | "launchBrowser": true,
7 | "launchUrl": "https://localhost:5100/api/time",
8 | "environmentVariables": {
9 | "ASPNETCORE_ENVIRONMENT": "Development"
10 | },
11 | "applicationUrl": "https://localhost:5100;http://localhost:5101"
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tests/WebApi/WebApi.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Test MVC Web API application.
4 | true
5 | $(NoWarn);SA1629
6 | MartinCostello.WebApi
7 | net9.0
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/tests/WebApi/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tests/WebApi/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "AllowedHosts": "*",
3 | "Logging": {
4 | "LogLevel": {
5 | "Default": "Information",
6 | "Microsoft.AspNetCore": "Warning"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------