├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ ├── apiversioning_rb_deploy.yml
│ ├── cb.yml
│ ├── exceptionhandling_rb_deploy.yml
│ ├── openapi_rb_deploy.yml
│ ├── rb.yml
│ ├── validation_rb_deploy.yml
│ └── vb.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── TechBuddy.Extensions.sln
├── readme.md
└── src
├── ApiVersioningExtension
├── ApiVersioningExtension.Tests
│ ├── ApiVersioningExtension.Tests.csproj
│ ├── GlobalUsings.cs
│ ├── Infrastructure
│ │ ├── Controllers
│ │ │ ├── v1
│ │ │ │ ├── TestController.cs
│ │ │ │ └── UrlSegmentTestController.cs
│ │ │ └── v2
│ │ │ │ ├── TestController.cs
│ │ │ │ └── UrlSegmentTestController.cs
│ │ ├── Helpers
│ │ │ └── CustomErrorResponseProvider.cs
│ │ └── Models
│ │ │ └── CustomErrorResponseModel.cs
│ ├── Tests
│ │ ├── ErrorResponseProviderTests.cs
│ │ ├── GeneralTests.cs
│ │ ├── HeaderReaderTests.cs
│ │ ├── MultiReaderTests.cs
│ │ ├── QueryStringReaderTests.cs
│ │ └── UrlSegmentReaderTests.cs
│ └── Usings.cs
└── ApiVersioningExtension
│ ├── ApiVersioningExtension.csproj
│ ├── Extensions
│ └── ApiVersioningDependencyInjectionExtension.cs
│ ├── Infrastructure
│ └── ConfigModels
│ │ └── ApiVersioningConfig.cs
│ ├── licenses
│ └── license.txt
│ ├── logo.png
│ └── readme.md
├── AspNetCoreExtensions.Tests.Common
└── AspNetCoreExtensions.Tests.Common
│ ├── AspNetCoreExtensions.Tests.Common.csproj
│ └── TestCommon
│ ├── Builders
│ └── HttpRequestBuilder.cs
│ └── Constants
│ ├── GeneralConstants.cs
│ └── TestConstants.cs
├── ExceptionHandlingExtension
├── ExceptionHandlingExtension.Tests
│ ├── ExceptionHandlingExtension.Tests.csproj
│ ├── ExceptionHandlingTests.cs
│ ├── Infrastructure
│ │ ├── Controllers
│ │ │ └── TestController.cs
│ │ └── Extensions
│ │ │ └── HttpContextExtensions.cs
│ └── Usings.cs
└── ExceptionHandlingExtension
│ ├── ExceptionHandlingExtension.csproj
│ ├── Extensions
│ └── ExceptionHandlingDependencyInjectionExtensions.cs
│ ├── Infrastructure
│ ├── ExceptionHandlers
│ │ └── DefaultExceptionHandler.cs
│ ├── ExceptionHandlingConstants.cs
│ ├── Extensions
│ │ └── HttpContextExtensions.cs
│ └── Models
│ │ ├── DefaultExceptionHandlerResponseModel.cs
│ │ └── OptionsModels
│ │ └── ExceptionHandlingOptions.cs
│ ├── licenses
│ └── license.txt
│ ├── logo.png
│ └── readme.md
├── SwaggerExtension
├── SwaggerExtension.Tests.WebApi
│ ├── Controllers
│ │ ├── v1
│ │ │ └── TestController.cs
│ │ └── v2
│ │ │ └── TestControllerV2.cs
│ ├── Infrastructure
│ │ └── Models
│ │ │ ├── BadRequestResponseModel.cs
│ │ │ └── TestModel.cs
│ ├── Program.cs
│ ├── Properties
│ │ └── launchSettings.json
│ ├── SwaggerExtension.Tests.WebApi.csproj
│ ├── SwaggerExtension.Tests.WebApi.xml
│ ├── appsettings.Development.json
│ └── appsettings.json
├── SwaggerExtension.Tests
│ ├── Helpers
│ │ └── DummyController.cs
│ ├── ResponseTypeModelProviderConfigTests.cs
│ ├── SwaggerConfigTests.cs
│ ├── SwaggerDocConfigTests.cs
│ ├── SwaggerExtension.Tests.csproj
│ └── Usings.cs
└── SwaggerExtension
│ ├── Extensions
│ └── SwaggerDependencyInjectionExtensions.cs
│ ├── Infrastructure
│ ├── ConfigModels
│ │ ├── DefaultHttpStatusCodeForHttpMethod.cs
│ │ ├── ResponseTypeModelProviderConfig.cs
│ │ ├── SwaggerBearerConfig.cs
│ │ ├── SwaggerConfig.cs
│ │ └── SwaggerDocConfig.cs
│ ├── Configurations
│ │ └── ConfigureSwaggerOptions.cs
│ ├── OperationFilters
│ │ ├── AddRequiredHeaderParameter.cs
│ │ ├── AuthenticationRequirements.cs
│ │ └── JsonIgnoreOperationFilter.cs
│ ├── ProduceResponseType
│ │ ├── ProduceResponseTypeModelProvider.cs
│ │ └── ResponseTypeModelProviderDependencyInjection.cs
│ └── SwaggerConstants.cs
│ ├── RoadMap.txt
│ ├── SwaggerExtension.csproj
│ ├── licenses
│ └── license.txt
│ ├── logo.png
│ └── readme.md
└── ValidationExtension
├── ValidationExtension.Tests.AF
├── .gitignore
├── Infrastructure
│ ├── Helpers
│ │ └── Validators
│ │ │ └── TestValidator.cs
│ └── Models
│ │ └── TestModel.cs
├── Properties
│ ├── launchSettings.json
│ ├── serviceDependencies.json
│ └── serviceDependencies.local.json
├── Startup.cs
├── TestFunction.cs
├── ValidationExtension.Tests.AF.csproj
└── host.json
├── ValidationExtension.Tests.WebApi
├── Controllers
│ └── TestController.cs
├── Infrastructure
│ ├── Models
│ │ ├── TestModel.cs
│ │ └── TestResponse.cs
│ ├── TestModelProvider.cs
│ └── Validators
│ │ └── TestValidator.cs
├── Program.cs
├── Properties
│ └── launchSettings.json
├── ValidationExtension.Tests.WebApi.csproj
├── appsettings.Development.json
└── appsettings.json
├── ValidationExtension.Tests
├── Infrastructure
│ ├── Controllers
│ │ └── TestController.cs
│ ├── Helpers
│ │ ├── ModelProviders
│ │ │ └── TestModelProvider.cs
│ │ └── Validators
│ │ │ ├── TestValidator.cs
│ │ │ └── TestValidatorV2.cs
│ └── Models
│ │ ├── ResponseModels
│ │ └── TestResponse.cs
│ │ ├── TestModel.cs
│ │ └── TestModelV2.cs
├── Tests
│ ├── HttpRequestTests.cs
│ ├── ValidationDIExtensionsTests.cs
│ ├── ValidationErrorResponseFactoryTests.cs
│ ├── ValidationErrorResponseModelTests.cs
│ ├── ValidationResultModelTests.cs
│ └── ValidationTests.cs
├── Usings.cs
└── ValidationExtension.Tests.csproj
└── ValidationExtension
├── Extensions
├── HttpRequestExtensions.cs
└── ValidationDependencyInjectionExtensions.cs
├── Infrastructure
├── ActionFilters
│ └── ValidateModelStateActionFilter.cs
├── Factories
│ └── ValidationErrorResponseFactory.cs
└── Models
│ ├── ConfigModels
│ └── ValidationExtensionConfig.cs
│ ├── ModelProviders
│ ├── DefaultModelProvider.cs
│ └── IDefaultModelProvider.cs
│ ├── ResponseModels
│ ├── BaseValidationErrorResponseModel.cs
│ └── DefaultValidationErrorResponseModel.cs
│ └── ValidationResultModel.cs
├── ValidationExtension.csproj
├── licenses
└── license.txt
├── logo.png
└── readme.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[BUG]"
5 | labels: bug
6 | assignees: salihcantekin
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See the error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Additional context**
27 | Add any other context about the problem here.
28 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: salihcantekin
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/apiversioning_rb_deploy.yml:
--------------------------------------------------------------------------------
1 | env:
2 | ProjectPath: src/ApiVersioningExtension/ApiVersioningExtension
3 | ProjectName: ApiVersioningExtension
4 | TagName: ApiVersioning
5 |
6 | name: Release with Tag (ApiVersioning.v*.*.*) & Push Nuget
7 |
8 | on:
9 | push:
10 | tags:
11 | - "ApiVersioning.v*.*.*"
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | timeout-minutes: 15
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v2
20 | #- name: Verify commit exists in origin/master
21 | # run: |
22 | # git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
23 | # git branch --remote --contains | grep origin/master
24 |
25 | - name: Get Version from TagName
26 | run: echo "VERSION=${GITHUB_REF#refs/tags/${{env.TagName}}.v}" >> $GITHUB_ENV
27 |
28 | - name: Print Version
29 | run: echo ${VERSION}
30 |
31 | - name: Build
32 | run: dotnet build --configuration Release /p:Version=${VERSION}
33 | - name: Test
34 | run: dotnet test --configuration Release /p:Version=${VERSION} --no-build
35 | - name: Pack
36 | run: dotnet pack ${{env.ProjectPath}}/${{env.ProjectName}}.csproj --configuration Release /p:Version=${VERSION} --no-build --output .
37 | - name: Push
38 | run: dotnet nuget push "*.nupkg" --source https://api.nuget.org/v3/index.json --api-key ${NUGET_API_KEY}
39 | env:
40 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
--------------------------------------------------------------------------------
/.github/workflows/cb.yml:
--------------------------------------------------------------------------------
1 | name: Development Validation Build
2 |
3 | on:
4 | push:
5 | branches:
6 | - dev
7 | pull_request:
8 | branches:
9 | - dev
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | timeout-minutes: 15
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v2
18 | - name: Build
19 | run: dotnet build --configuration Release
20 | - name: Test
21 | run: dotnet test --configuration Release --no-build
--------------------------------------------------------------------------------
/.github/workflows/exceptionhandling_rb_deploy.yml:
--------------------------------------------------------------------------------
1 | env:
2 | ProjectPath: src/ExceptionHandlingExtension/ExceptionHandlingExtension
3 | ProjectName: ExceptionHandlingExtension
4 | TagName: ExceptionHandling
5 |
6 | name: Release with Tag (ExceptionHandling.v*.*.*) & Push Nuget
7 |
8 | on:
9 | push:
10 | tags:
11 | - "ExceptionHandling.v*.*.*"
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | timeout-minutes: 15
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v2
20 | #- name: Verify commit exists in origin/master
21 | # run: |
22 | # git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
23 | # git branch --remote --contains | grep origin/master
24 |
25 | - name: Get Version from TagName
26 | run: echo "VERSION=${GITHUB_REF#refs/tags/${{env.TagName}}.v}" >> $GITHUB_ENV
27 |
28 | - name: Print Version
29 | run: echo ${VERSION}
30 |
31 | - name: Build
32 | run: dotnet build --configuration Release /p:Version=${VERSION}
33 | - name: Test
34 | run: dotnet test --configuration Release /p:Version=${VERSION} --no-build
35 | - name: Pack
36 | run: dotnet pack ${{env.ProjectPath}}/${{env.ProjectName}}.csproj --configuration Release /p:Version=${VERSION} --no-build --output .
37 | - name: Push
38 | run: dotnet nuget push "*.nupkg" --source https://api.nuget.org/v3/index.json --api-key ${NUGET_API_KEY}
39 | env:
40 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
--------------------------------------------------------------------------------
/.github/workflows/openapi_rb_deploy.yml:
--------------------------------------------------------------------------------
1 | env:
2 | ProjectPath: src/SwaggerExtension/SwaggerExtension
3 | ProjectName: SwaggerExtension
4 | TagName: OpenApi
5 |
6 | name: Release with Tag (OpenApi.v*.*.*) & Push Nuget
7 |
8 | on:
9 | push:
10 | tags:
11 | - "OpenApi.v*.*.*"
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | timeout-minutes: 15
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v2
20 | #- name: Verify commit exists in origin/master
21 | # run: |
22 | # git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
23 | # git branch --remote --contains | grep origin/master
24 |
25 | - name: Get Version from TagName
26 | run: echo "VERSION=${GITHUB_REF#refs/tags/${{env.TagName}}.v}" >> $GITHUB_ENV
27 |
28 | - name: Print Version
29 | run: echo ${VERSION}
30 |
31 | - name: Build
32 | run: dotnet build --configuration Release /p:Version=${VERSION}
33 | - name: Test
34 | run: dotnet test --configuration Release /p:Version=${VERSION} --no-build
35 | - name: Pack
36 | run: dotnet pack ${{env.ProjectPath}}/${{env.ProjectName}}.csproj --configuration Release /p:Version=${VERSION} --no-build --output .
37 | - name: Push
38 | run: dotnet nuget push "*.nupkg" --source https://api.nuget.org/v3/index.json --api-key ${NUGET_API_KEY}
39 | env:
40 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
--------------------------------------------------------------------------------
/.github/workflows/rb.yml:
--------------------------------------------------------------------------------
1 | name: Master Validation Build
2 | on:
3 | push:
4 | branches:
5 | - master
6 | pull_request:
7 | branches:
8 | - master
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | timeout-minutes: 15
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v2
17 | - name: Build
18 | run: dotnet build --configuration Release
19 | - name: Test
20 | run: dotnet test --configuration Release --no-build
21 |
--------------------------------------------------------------------------------
/.github/workflows/validation_rb_deploy.yml:
--------------------------------------------------------------------------------
1 | env:
2 | ProjectPath: src/ValidationExtension/ValidationExtension
3 | ProjectName: ValidationExtension
4 | TagName: Validation
5 |
6 | name: Release with Tag (Validation.v*.*.*) & Push Nuget
7 |
8 | on:
9 | push:
10 | tags:
11 | - "Validation.v*.*.*"
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | timeout-minutes: 15
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v2
20 | #- name: Verify commit exists in origin/master
21 | # run: |
22 | # git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
23 | # git branch --remote --contains | grep origin/master
24 |
25 | - name: Get Version from TagName
26 | run: echo "VERSION=${GITHUB_REF#refs/tags/${{env.TagName}}.v}" >> $GITHUB_ENV
27 |
28 | - name: Print Version
29 | run: echo ${VERSION}
30 |
31 | - name: Build
32 | run: dotnet build --configuration Release /p:Version=${VERSION}
33 | - name: Test
34 | run: dotnet test --configuration Release /p:Version=${VERSION} --no-build
35 | - name: Pack
36 | run: dotnet pack ${{env.ProjectPath}}/${{env.ProjectName}}.csproj --configuration Release /p:Version=${VERSION} --no-build --output .
37 | - name: Push
38 | run: dotnet nuget push "*.nupkg" --source https://api.nuget.org/v3/index.json --api-key ${NUGET_API_KEY}
39 | env:
40 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
41 |
--------------------------------------------------------------------------------
/.github/workflows/vb.yml:
--------------------------------------------------------------------------------
1 | name: Feature Validation Build
2 |
3 | on:
4 | push:
5 | branches:
6 | - feature/*
7 | - bug/*
8 | - bugfix/*
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | timeout-minutes: 15
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v2
17 | - name: Build
18 | run: dotnet build --configuration Release
19 | - name: Test
20 | run: dotnet test --configuration Release --no-build
21 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | salihcantekin@gmail.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 |
2 | > PS: Check the issues section first if the bug/feature already reported/suggested. If not, please follow the order below;
3 |
4 | - Fork this repo
5 | - Create a branch for the development. If you're fixing a bug, create bug/[DESCRIPTION] branch. If this is a new feature, create feature/[DESCRIPTION].
6 | - Create Pull Request from that branch
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Tech Buddy Youtube
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | []()
2 |
3 | ### Repo Stat
4 |
5 | [](https://github.com/TechBuddyTR/TechBuddy.Extensions/commits/dev)
6 | [](https://github.com/TechBuddyTR/TechBuddy.Extensions/graphs/contributors)
7 | [](https://github.com/TechBuddyTR/TechBuddy.Extensions/pulls)
8 | [](https://github.com/TechBuddyTR/TechBuddy.Extensions/pulls?q=is%3Apr+is%3Aclosed)
9 | [](https://github.com/TechBuddyTR/TechBuddy.Extensions/issues)
10 | [](https://github.com/TechBuddyTR/TechBuddy.Extensions/issues?q=is%3Aissue+is%3Aclosed)
11 |
12 | []()
13 | []()
14 | []()
15 |
16 |
17 |
18 | ## Introduction
19 |
20 | This solution is to contribute open-source community. We are aiming to create Extension libraries (nuget packages) for AspNet projects.
21 | There are many common patterns/tools that most of us are using mostly on our WebApi projects.
22 | With the nuget packages provided by the projects under this solution, it will be easy to implement those libraries with their default configurations.
23 | Not only it will be only a single line of code to use them with default configs, but also you will be able to customize them.
24 |
25 | > PS: This project is being developed by the Community of TechBuddyTR [Youtube Channel](https://www.youtube.com/c/TechBuddyTR)
26 |
27 |
28 | ### Packages
29 |
30 | | Package Name | Package | Download |
31 | | ------------- | ------------- | ------------- |
32 | | ApiVersioning | [](https://www.nuget.org/packages/TechBuddy.Extensions.AspNetCore.ApiVersioning) | [](https://www.nuget.org/packages/TechBuddy.Extensions.AspNetCore.ApiVersioning/) |
33 | | Validation | [](https://www.nuget.org/packages/TechBuddy.Extensions.Validation) | [](https://www.nuget.org/packages/TechBuddy.Extensions.Validation/) |
34 | | ExceptionHandling | [](https://www.nuget.org/packages/TechBuddy.Extensions.AspNetCore.ExceptionHandling) | [](https://www.nuget.org/packages/TechBuddy.Extensions.AspNetCore.ExceptionHandling/) |
35 | | OpenApi | [](https://www.nuget.org/packages/TechBuddy.Extensions.OpenApi) | [](https://www.nuget.org/packages/TechBuddy.Extensions.OpenApi) |
36 |
37 |
38 | ### Extensions
39 |
40 | There are four different extension libraries planned which are;
41 |
42 | - [ApiVersioning](https://github.com/TechBuddyTR/TechBuddy.Extensions/tree/dev/src/ApiVersioningExtension/ApiVersioningExtension)
43 | - [Global Exception Handling](https://github.com/TechBuddyTR/TechBuddy.Extensions/tree/dev/src/ExceptionHandlingExtension/ExceptionHandlingExtension)
44 | - [Validations (FluentValidation based)](https://github.com/TechBuddyTR/TechBuddy.Extensions/tree/dev/src/ValidationExtension/ValidationExtension)
45 | - [OpenApi Support (Swagger, Swagger UI)](https://github.com/TechBuddyTR/TechBuddy.Extensions/tree/dev/src/SwaggerExtension/SwaggerExtension)
46 |
47 |
48 | Every extension library will have their own NuGet Packages. Please find the details under the project folders.
49 |
50 | ----
51 |
52 |
53 | #### General feedback and discussions
54 | Please start a discussion on the [repo issue tracker](https://github.com/TechBuddyTR/TechBuddy.Extensions/issues)
55 |
56 | #### Contribution
57 |
58 | To contribute, please fork the repository and do the changes on that repository. After that you can create a pull request to development branch on the original repository.
59 | As you can do that, this is not the only way to contribute. You can also raise an issue if you see anything wrong/unexpected.
60 |
61 | > PS: Please consider creating UnitTests for the development you have made.
62 |
63 |
64 |
65 | **License, etc.**
66 |
67 | TechBuddy.Extensions is Copyright © 2022 [Salih Cantekin](https://github.com/salihcantekin) and other contributors under the MIT license.
68 |
--------------------------------------------------------------------------------
/src/ApiVersioningExtension/ApiVersioningExtension.Tests/ApiVersioningExtension.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 |
7 | false
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/ApiVersioningExtension/ApiVersioningExtension.Tests/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using FluentAssertions;
2 | global using Microsoft.AspNetCore.Builder;
3 | global using Microsoft.AspNetCore.Hosting;
4 | global using Microsoft.AspNetCore.Mvc;
5 | global using Microsoft.AspNetCore.TestHost;
6 | global using Microsoft.Extensions.DependencyInjection;
7 | global using TechBuddy.Extensions.AspNetCore.ApiVersioning;
8 |
--------------------------------------------------------------------------------
/src/ApiVersioningExtension/ApiVersioningExtension.Tests/Infrastructure/Controllers/v1/TestController.cs:
--------------------------------------------------------------------------------
1 | namespace ApiVersioningExtension.Tests.Infrastructure.Controllers.v1;
2 |
3 | [ApiController]
4 | [Route("api/[controller]")]
5 | [ApiVersion("1.0")]
6 | public class TestController : ControllerBase
7 | {
8 | [HttpGet()]
9 | public ActionResult Get()
10 | {
11 | return Ok("V1");
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/ApiVersioningExtension/ApiVersioningExtension.Tests/Infrastructure/Controllers/v1/UrlSegmentTestController.cs:
--------------------------------------------------------------------------------
1 | using TechBuddy.Extensions.Tests.Common.TestCommon.Constants;
2 |
3 | namespace ApiVersioningExtension.Tests.Infrastructure.Controllers.v1;
4 |
5 |
6 | [ApiController]
7 | [Route(TestConstants.ControllerRoute)]
8 | [ApiVersion("1.0")]
9 | public sealed class UrlSegmentTestController : ControllerBase
10 | {
11 | [HttpGet()]
12 | public ActionResult Get()
13 | {
14 | return Ok("V1");
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/ApiVersioningExtension/ApiVersioningExtension.Tests/Infrastructure/Controllers/v2/TestController.cs:
--------------------------------------------------------------------------------
1 | namespace ApiVersioningExtension.Tests.Infrastructure.Controllers.v2;
2 |
3 | [ApiController]
4 | [Route("api/[controller]")]
5 | [ApiVersion("2.0")]
6 | public class TestController : ControllerBase
7 | {
8 | [HttpGet]
9 | public ActionResult Get()
10 | {
11 | return Ok("V2");
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/ApiVersioningExtension/ApiVersioningExtension.Tests/Infrastructure/Controllers/v2/UrlSegmentTestController.cs:
--------------------------------------------------------------------------------
1 | using TechBuddy.Extensions.Tests.Common.TestCommon.Constants;
2 |
3 | namespace ApiVersioningExtension.Tests.Infrastructure.Controllers.v2;
4 |
5 |
6 | [ApiController]
7 | [Route(TestConstants.ControllerRoute)]
8 | [ApiVersion("2.0")]
9 | public sealed class UrlSegmentTestController : ControllerBase
10 | {
11 | [HttpGet()]
12 | public ActionResult Get()
13 | {
14 | return Ok("V2");
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/ApiVersioningExtension/ApiVersioningExtension.Tests/Infrastructure/Helpers/CustomErrorResponseProvider.cs:
--------------------------------------------------------------------------------
1 | using ApiVersioningExtension.Tests.Infrastructure.Models;
2 | using Microsoft.AspNetCore.Http;
3 | using Microsoft.AspNetCore.Mvc.Versioning;
4 |
5 | namespace ApiVersioningExtension.Tests.Infrastructure.Helpers;
6 | internal class CustomErrorResponseProvider : IErrorResponseProvider
7 | {
8 | public IActionResult CreateResponse(ErrorResponseContext context)
9 | {
10 | var response = new CustomErrorResponseModel()
11 | {
12 | ErrorCode = context.ErrorCode,
13 | ErrorMessage = "ApiVersioning Exception",
14 | ErrorDetail = context.Message
15 | };
16 |
17 | var result = new ObjectResult(response)
18 | {
19 | StatusCode = StatusCodes.Status400BadRequest
20 | };
21 |
22 | return result;
23 | }
24 | }
--------------------------------------------------------------------------------
/src/ApiVersioningExtension/ApiVersioningExtension.Tests/Infrastructure/Models/CustomErrorResponseModel.cs:
--------------------------------------------------------------------------------
1 | namespace ApiVersioningExtension.Tests.Infrastructure.Models;
2 | internal class CustomErrorResponseModel
3 | {
4 | public string ErrorCode { get; set; }
5 |
6 | public string ErrorMessage { get; set; }
7 |
8 | public string ErrorDetail { get; set; }
9 | }
10 |
--------------------------------------------------------------------------------
/src/ApiVersioningExtension/ApiVersioningExtension.Tests/Tests/ErrorResponseProviderTests.cs:
--------------------------------------------------------------------------------
1 | using ApiVersioningExtension.Tests.Infrastructure.Helpers;
2 | using ApiVersioningExtension.Tests.Infrastructure.Models;
3 | using System.Net.Http.Json;
4 | using System.Text.Json.Nodes;
5 | using TechBuddy.Extensions.Tests.Common.TestCommon.Constants;
6 |
7 | namespace ApiVersioningExtension.Tests.Tests;
8 | internal class ErrorResponseProviderTests
9 | {
10 | [Test]
11 | public async Task ErrorResponse_WithNoErrorResponseProvider_ShoudReturnDefaultModel()
12 | {
13 | // Arrange
14 | var errorResponseTestServer = GetErrorResponseTestServer();
15 | var errorResponseClient = errorResponseTestServer.CreateClient();
16 |
17 | var request = new HttpRequestMessage(HttpMethod.Get, $"/api/Test?{TestConstants.ApiversionKey}=1.0");
18 | request.Headers.Add(TestConstants.ApiversionKey, "2.0");
19 |
20 | // Action
21 | var response = await errorResponseClient.SendAsync(request);
22 | var respo = await response.Content.ReadAsStringAsync();
23 | var responseJsonObject = JsonNode.Parse(respo).AsObject().First().Value.AsObject();
24 |
25 |
26 | // Assert
27 | responseJsonObject.ContainsKey("code").Should().BeTrue();
28 | responseJsonObject.ContainsKey("message").Should().BeTrue();
29 | responseJsonObject["code"].ToString().Should().Be("AmbiguousApiVersion");
30 | }
31 |
32 |
33 | [Test]
34 | public async Task ErrorResponse_WithCustomErrorResponseProvider_ShoudReturnCustomModel()
35 | {
36 | // Arrange
37 | var errorResponseTestServer = GetErrorResponseTestServer(useCustomProvider: true);
38 | var errorResponseClient = errorResponseTestServer.CreateClient();
39 |
40 | var expectedModel = new CustomErrorResponseModel()
41 | {
42 | ErrorMessage = "ApiVersioning Exception",
43 | ErrorCode = "AmbiguousApiVersion"
44 | };
45 |
46 | var request = new HttpRequestMessage(HttpMethod.Get, $"/api/Test?{TestConstants.ApiversionKey}=1.0");
47 | request.Headers.Add(TestConstants.ApiversionKey, "2.0");
48 |
49 | // Action
50 | var response = await errorResponseClient.SendAsync(request);
51 |
52 | var resultModel = await response.Content.ReadFromJsonAsync();
53 |
54 | // Assert
55 | resultModel.Should().BeEquivalentTo(expectedModel, opt => opt.Excluding(p => p.ErrorDetail));
56 | }
57 |
58 |
59 | #region Private Methods
60 |
61 | private TestServer GetErrorResponseTestServer(bool useCustomProvider = false)
62 | {
63 | var hostBuilder = new WebHostBuilder()
64 | .ConfigureServices(services =>
65 | {
66 | services.AddMvc(i => i.EnableEndpointRouting = false);
67 |
68 | services.AddTechBuddyApiVersioning(config =>
69 | {
70 | config.AssumeDefaultVersionWhenUnspecified = true;
71 |
72 | if (useCustomProvider)
73 | config.UseErrorResponseProvider();
74 |
75 | config.AddHeaderApiVersionReader(TestConstants.ApiversionKey);
76 | config.AddQueryStringApiVersionReader(TestConstants.ApiversionKey);
77 | });
78 | })
79 | .Configure(app =>
80 | {
81 | app.UseMvc();
82 | });
83 |
84 | return new TestServer(hostBuilder);
85 | }
86 |
87 | #endregion
88 | }
89 |
--------------------------------------------------------------------------------
/src/ApiVersioningExtension/ApiVersioningExtension.Tests/Tests/GeneralTests.cs:
--------------------------------------------------------------------------------
1 | using TechBuddy.Extensions.Tests.Common.TestCommon.Constants;
2 |
3 | namespace ApiVersioningExtension.Tests.Tests;
4 | public sealed class GeneralTests
5 | {
6 | private TestServer defaultVersionTestServer;
7 |
8 | [OneTimeSetUp]
9 | public void SetUp()
10 | {
11 | defaultVersionTestServer = GetDefaultVersionTestServer();
12 | }
13 |
14 |
15 | #region DefaultVersion Tests With Config
16 |
17 | [Test]
18 | public async Task WhenAssumeDefaultVersionWhenUnspecified_TRUE_ShouldUseDefaultVersion()
19 | {
20 | // Arrange
21 | var client = defaultVersionTestServer.CreateClient();
22 |
23 | var request = new HttpRequestMessage(HttpMethod.Get, $"/api/Test");
24 |
25 | // Action
26 | HttpResponseMessage response = await client.SendAsync(request);
27 | var responseContent = await response.Content.ReadAsStringAsync();
28 |
29 | //Assert
30 | responseContent.Should().Be("V2");
31 | }
32 |
33 | [Test]
34 | public async Task WhenAssumeDefaultVersionWhenUnspecified_FALSE_ShouldReturnBadRequest()
35 | {
36 | // Arrange
37 | var server = GetDefaultVersionTestServer("1.0", false);
38 | var client = server.CreateClient();
39 |
40 | var request = new HttpRequestMessage(HttpMethod.Get, $"/api/Test");
41 |
42 | // Action
43 | HttpResponseMessage response = await client.SendAsync(request);
44 |
45 | //Assert
46 | response.StatusCode.Should().Be(System.Net.HttpStatusCode.BadRequest);
47 | }
48 |
49 | [Test]
50 | public async Task WhenAssumeDefaultVersionWhenUnspecified_FALSE_ShouldUseNOTDefaultVersion()
51 | {
52 | // Arrange
53 | var server = GetDefaultVersionTestServer("1.0", false);
54 | var client = server.CreateClient();
55 |
56 | var request = new HttpRequestMessage(HttpMethod.Get, $"/api/Test?{TestConstants.ApiversionKey}=2.0");
57 |
58 | // Action
59 | HttpResponseMessage response = await client.SendAsync(request);
60 | var responseContent = await response.Content.ReadAsStringAsync();
61 |
62 | //Assert
63 | response.IsSuccessStatusCode.Should().BeTrue();
64 | responseContent.Should().Be("V2");
65 | }
66 |
67 | #endregion
68 |
69 | #region DefaultVersion Tests With Default Config
70 |
71 | [Test]
72 | public async Task DefaultConfig_WithNoApiVersioning_ShouldUseDefaultVersion()
73 | {
74 | // Arrange
75 | var server = GetTestServerDefaultConfig();
76 | var client = server.CreateClient();
77 |
78 | var request = new HttpRequestMessage(HttpMethod.Get, $"/api/Test");
79 |
80 | // Action
81 | HttpResponseMessage response = await client.SendAsync(request);
82 | var responseContent = await response.Content.ReadAsStringAsync();
83 |
84 | //Assert
85 | responseContent.Should().Be("V1");
86 | }
87 |
88 | [Test]
89 | public async Task DefaultConfig_WithNoApiVersioning_ShouldReturnOk()
90 | {
91 | // Arrange
92 | var server = GetTestServerDefaultConfig();
93 | var client = server.CreateClient();
94 |
95 | var request = new HttpRequestMessage(HttpMethod.Get, $"/api/Test");
96 |
97 | // Action
98 | HttpResponseMessage response = await client.SendAsync(request);
99 |
100 | //Assert
101 | response.StatusCode.Should().Be(System.Net.HttpStatusCode.OK);
102 | }
103 |
104 | #endregion
105 |
106 |
107 |
108 | #region Private Methods
109 |
110 | private static TestServer GetDefaultVersionTestServer(string defaultVersion = "2.0", bool assume = true)
111 | {
112 | var hostBuilder = new WebHostBuilder()
113 | .ConfigureServices(services =>
114 | {
115 | services.AddMvc(i => i.EnableEndpointRouting = false);
116 |
117 | services.AddTechBuddyApiVersioning(config =>
118 | {
119 | config.AddQueryStringApiVersionReader(TestConstants.ApiversionKey);
120 | config.AssumeDefaultVersionWhenUnspecified = assume;
121 | config.DefaultApiVersion = defaultVersion;
122 | });
123 | })
124 | .Configure(app =>
125 | {
126 | app.UseMvc();
127 | });
128 |
129 | return new TestServer(hostBuilder);
130 | }
131 |
132 | private static TestServer GetTestServerDefaultConfig()
133 | {
134 | var hostBuilder = new WebHostBuilder()
135 | .ConfigureServices(services =>
136 | {
137 | services.AddMvc(i => i.EnableEndpointRouting = false);
138 |
139 | services.AddTechBuddyApiVersioning();
140 | })
141 | .Configure(app =>
142 | {
143 | app.UseMvc();
144 | });
145 |
146 | return new TestServer(hostBuilder);
147 | }
148 |
149 | #endregion
150 | }
151 |
--------------------------------------------------------------------------------
/src/ApiVersioningExtension/ApiVersioningExtension.Tests/Tests/HeaderReaderTests.cs:
--------------------------------------------------------------------------------
1 | using TechBuddy.Extensions.Tests.Common.TestCommon.Constants;
2 |
3 | namespace ApiVersioningExtension.Tests.Tests;
4 |
5 | public class HeaderReaderTests
6 | {
7 | private TestServer headerReaderTestServer;
8 |
9 | private HttpClient headerReaderClient;
10 |
11 | [OneTimeSetUp]
12 | public void Setup()
13 | {
14 | headerReaderTestServer = GetHeaderReaderTestServer();
15 | headerReaderClient = headerReaderTestServer.CreateClient();
16 | }
17 |
18 | [Test]
19 | public async Task ApiVersioningHeaderReader_WithV1_ShoudPass()
20 | {
21 | // Arrange
22 | var request = new HttpRequestMessage(HttpMethod.Get, "/api/Test");
23 | request.Headers.Add(TestConstants.ApiversionKey, "1.0");
24 |
25 | // Action
26 | HttpResponseMessage response = await headerReaderClient.SendAsync(request);
27 | var responseContent = await response.Content.ReadAsStringAsync();
28 |
29 | // Assert
30 | response.IsSuccessStatusCode.Should().BeTrue();
31 | responseContent.Should().Be("V1");
32 | }
33 |
34 |
35 | [Test]
36 | public async Task ApiVersioningHeaderReader_WithV2_ShoudPass()
37 | {
38 | // Arrange
39 | var request = new HttpRequestMessage(HttpMethod.Get, "/api/Test");
40 | request.Headers.Add(TestConstants.ApiversionKey, "2.0");
41 |
42 |
43 | // Action
44 | HttpResponseMessage response = await headerReaderClient.SendAsync(request);
45 | var responseContent = await response.Content.ReadAsStringAsync();
46 |
47 | // Assert
48 | response.IsSuccessStatusCode.Should().BeTrue();
49 | responseContent.Should().Be("V2");
50 | }
51 |
52 |
53 |
54 |
55 | [Test]
56 | public async Task ApiVersioningHeaderReader_WithNoVersion_ShoudReturnBadRequest()
57 | {
58 | // Arrange
59 | var request = new HttpRequestMessage(HttpMethod.Get, "/api/Test");
60 |
61 | // Action
62 | HttpResponseMessage response = await headerReaderClient.SendAsync(request);
63 |
64 |
65 | // Assert
66 | response.IsSuccessStatusCode.Should().BeFalse();
67 | response.StatusCode.Should().Be(System.Net.HttpStatusCode.BadRequest);
68 | }
69 |
70 | [Test]
71 | public async Task ApiVersioningHeaderReader_WithInvalidVersion_ShoudReturnBadRequest()
72 | {
73 | // Arrange
74 | var request = new HttpRequestMessage(HttpMethod.Get, "/api/Test");
75 | request.Headers.Add(TestConstants.ApiversionKey, "5.0");
76 |
77 | // Action
78 | HttpResponseMessage response = await headerReaderClient.SendAsync(request);
79 |
80 |
81 | // Assert
82 | response.IsSuccessStatusCode.Should().BeFalse();
83 | response.StatusCode.Should().Be(System.Net.HttpStatusCode.BadRequest);
84 | }
85 |
86 |
87 | #region Private Methods
88 |
89 | private TestServer GetHeaderReaderTestServer()
90 | {
91 | if (headerReaderTestServer is not null)
92 | return headerReaderTestServer;
93 |
94 | var hostBuilder = new WebHostBuilder()
95 | .ConfigureServices(services =>
96 | {
97 | services.AddMvc(i => i.EnableEndpointRouting = false);
98 |
99 | services.AddTechBuddyApiVersioning(config =>
100 | {
101 | config.AssumeDefaultVersionWhenUnspecified = false;
102 |
103 | config.AddHeaderApiVersionReader(TestConstants.ApiversionKey);
104 | });
105 | })
106 | .Configure(app =>
107 | {
108 | app.UseMvc();
109 | });
110 |
111 | headerReaderTestServer = new TestServer(hostBuilder);
112 |
113 | return headerReaderTestServer;
114 | }
115 |
116 | #endregion
117 | }
--------------------------------------------------------------------------------
/src/ApiVersioningExtension/ApiVersioningExtension.Tests/Tests/MultiReaderTests.cs:
--------------------------------------------------------------------------------
1 | using TechBuddy.Extensions.Tests.Common.TestCommon.Constants;
2 |
3 | namespace ApiVersioningExtension.Tests.Tests;
4 | internal class MultiReaderTests
5 | {
6 | private TestServer multiReaderTestServer;
7 |
8 | private HttpClient multiReaderClient;
9 |
10 |
11 | [OneTimeSetUp]
12 | public void Setup()
13 | {
14 | multiReaderTestServer = GetMultiReaderTestServer();
15 | multiReaderClient = multiReaderTestServer.CreateClient();
16 | }
17 |
18 |
19 | [Test]
20 | public async Task MultiReader_WithNoVersion_ShoudReturnBadRequest()
21 | {
22 | // Arrange
23 | var request = new HttpRequestMessage(HttpMethod.Get, "/api/Test");
24 |
25 | // Action
26 | HttpResponseMessage response = await multiReaderClient.SendAsync(request);
27 |
28 |
29 | // Assert
30 | response.IsSuccessStatusCode.Should().BeFalse();
31 | response.StatusCode.Should().Be(System.Net.HttpStatusCode.BadRequest);
32 | }
33 |
34 | [Test]
35 | public async Task MultiReader_WithV1_ShoudPass()
36 | {
37 | // Arrange
38 | var request = new HttpRequestMessage(HttpMethod.Get, $"/api/Test?{TestConstants.ApiversionKey}=1.0");
39 |
40 | // Action
41 | HttpResponseMessage response = await multiReaderClient.SendAsync(request);
42 | var responseContent = await response.Content.ReadAsStringAsync();
43 |
44 |
45 | // Assert
46 | response.IsSuccessStatusCode.Should().BeTrue();
47 | responseContent.Should().Be("V1");
48 | }
49 |
50 |
51 | [Test]
52 | public async Task MultiReader_WithDifferentVersions_ShoudReturnBadRequest()
53 | {
54 | // Arrange
55 | var request = new HttpRequestMessage(HttpMethod.Get, $"/api/Test?{TestConstants.ApiversionKey}=1.0");
56 | request.Headers.Add(TestConstants.ApiversionKey, "2.0");
57 |
58 | // Action
59 | var response = await multiReaderClient.SendAsync(request);
60 |
61 | // Assert
62 | response.IsSuccessStatusCode.Should().BeFalse();
63 | response.StatusCode.Should().Be(System.Net.HttpStatusCode.BadRequest);
64 | }
65 |
66 | [Test]
67 | public async Task MultiReader_WithSameVersions_ShoudReturnOk()
68 | {
69 | // Arrange
70 | var request = new HttpRequestMessage(HttpMethod.Get, $"/api/Test?{TestConstants.ApiversionKey}=2.0");
71 | request.Headers.Add(TestConstants.ApiversionKey, "2.0");
72 |
73 | // Action
74 | var response = await multiReaderClient.SendAsync(request);
75 |
76 | // Assert
77 | response.IsSuccessStatusCode.Should().BeTrue();
78 | response.StatusCode.Should().Be(System.Net.HttpStatusCode.OK);
79 | }
80 |
81 |
82 |
83 | #region Private Methods
84 |
85 | private TestServer GetMultiReaderTestServer()
86 | {
87 | if (multiReaderTestServer is not null)
88 | return multiReaderTestServer;
89 |
90 | var hostBuilder = new WebHostBuilder()
91 | .ConfigureServices(services =>
92 | {
93 | services.AddMvc(i => i.EnableEndpointRouting = false);
94 |
95 | services.AddTechBuddyApiVersioning(config =>
96 | {
97 | config.AssumeDefaultVersionWhenUnspecified = false;
98 |
99 | config.AddHeaderApiVersionReader(TestConstants.ApiversionKey);
100 | config.AddQueryStringApiVersionReader(TestConstants.ApiversionKey);
101 | });
102 | })
103 | .Configure(app =>
104 | {
105 | app.UseMvc();
106 | });
107 |
108 | multiReaderTestServer = new TestServer(hostBuilder);
109 |
110 | return multiReaderTestServer;
111 | }
112 |
113 | #endregion
114 |
115 |
116 | }
117 |
--------------------------------------------------------------------------------
/src/ApiVersioningExtension/ApiVersioningExtension.Tests/Tests/QueryStringReaderTests.cs:
--------------------------------------------------------------------------------
1 | using TechBuddy.Extensions.Tests.Common.TestCommon.Constants;
2 |
3 | namespace ApiVersioningExtension.Tests.Tests;
4 |
5 | public class QueryStringReaderTests
6 | {
7 |
8 | private TestServer queryStringReaderTestServer;
9 |
10 | private HttpClient queryStringReaderClient;
11 |
12 | [OneTimeSetUp]
13 | public void Setup()
14 | {
15 | queryStringReaderTestServer = GetQueryStringReaderTestServer();
16 | queryStringReaderClient = queryStringReaderTestServer.CreateClient();
17 | }
18 |
19 |
20 | [Test]
21 | public async Task ApiVersioningQueryStringReader_WithV1_ShoudPass()
22 | {
23 | // Arrange
24 | var request = new HttpRequestMessage(HttpMethod.Get, $"/api/Test?{TestConstants.ApiversionKey}=1.0");
25 |
26 | // Action
27 | HttpResponseMessage response = await queryStringReaderClient.SendAsync(request);
28 | var responseContent = await response.Content.ReadAsStringAsync();
29 |
30 |
31 | // Assert
32 | response.IsSuccessStatusCode.Should().BeTrue();
33 | responseContent.Should().Be("V1");
34 | }
35 |
36 | [Test]
37 | public async Task ApiVersioningQueryStringReader_WithV2_ShoudPass()
38 | {
39 | // Arrange
40 | var request = new HttpRequestMessage(HttpMethod.Get, $"/api/Test?{TestConstants.ApiversionKey}=2.0");
41 |
42 |
43 | // Action
44 | HttpResponseMessage response = await queryStringReaderClient.SendAsync(request);
45 | var responseContent = await response.Content.ReadAsStringAsync();
46 |
47 | // Assert
48 | response.IsSuccessStatusCode.Should().BeTrue();
49 | responseContent.Should().Be("V2");
50 | }
51 |
52 | [Test]
53 | public async Task ApiVersioningQueryStringReader_WithInvalidVersion_ShoudReturnBadRequest()
54 | {
55 | // Arrange
56 | var request = new HttpRequestMessage(HttpMethod.Get, $"/api/Test?{TestConstants.ApiversionKey}=5.0");
57 |
58 |
59 | // Action
60 | HttpResponseMessage response = await queryStringReaderClient.SendAsync(request);
61 |
62 | // Assert
63 | response.IsSuccessStatusCode.Should().BeFalse();
64 | response.StatusCode.Should().Be(System.Net.HttpStatusCode.BadRequest);
65 | }
66 |
67 |
68 | [Test]
69 | public async Task ApiVersioningQueryStringReader_WithNoVersion_ShoudReturnBadRequest()
70 | {
71 | // Arrange
72 | var request = new HttpRequestMessage(HttpMethod.Get, "/api/Test");
73 |
74 | // Action
75 | HttpResponseMessage response = await queryStringReaderClient.SendAsync(request);
76 |
77 |
78 | // Assert
79 | response.IsSuccessStatusCode.Should().BeFalse();
80 | response.StatusCode.Should().Be(System.Net.HttpStatusCode.BadRequest);
81 | }
82 |
83 |
84 |
85 | #region Private Methods
86 |
87 | private TestServer GetQueryStringReaderTestServer()
88 | {
89 | if (queryStringReaderTestServer is not null)
90 | return queryStringReaderTestServer;
91 |
92 | var hostBuilder = new WebHostBuilder()
93 | .ConfigureServices(services =>
94 | {
95 | services.AddMvc(i => i.EnableEndpointRouting = false);
96 |
97 | services.AddTechBuddyApiVersioning(config =>
98 | {
99 | config.AssumeDefaultVersionWhenUnspecified = false;
100 |
101 | config.AddQueryStringApiVersionReader(TestConstants.ApiversionKey);
102 | });
103 | })
104 | .Configure(app =>
105 | {
106 | app.UseMvc();
107 | });
108 |
109 | queryStringReaderTestServer = new TestServer(hostBuilder);
110 |
111 | return queryStringReaderTestServer;
112 | }
113 |
114 | #endregion
115 | }
--------------------------------------------------------------------------------
/src/ApiVersioningExtension/ApiVersioningExtension.Tests/Tests/UrlSegmentReaderTests.cs:
--------------------------------------------------------------------------------
1 | namespace ApiVersioningExtension.Tests.Tests;
2 | public sealed class UrlSegmentReaderTests
3 | {
4 | private TestServer urlSegmentReaderTestServer;
5 |
6 | private HttpClient urlSegmentReaderClient;
7 |
8 | [OneTimeSetUp]
9 | public void Setup()
10 | {
11 | urlSegmentReaderTestServer = GetUrlSegmentReaderTestServer();
12 | urlSegmentReaderClient = urlSegmentReaderTestServer.CreateClient();
13 | }
14 |
15 |
16 | [Test]
17 | public async Task ApiVersioningUrlSegmentReader_WithV1_ShoudPass()
18 | {
19 | // Arrange
20 | var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/UrlSegmentTest");
21 |
22 | // Action
23 | HttpResponseMessage response = await urlSegmentReaderClient.SendAsync(request);
24 | var responseContent = await response.Content.ReadAsStringAsync();
25 |
26 |
27 | // Assert
28 | response.IsSuccessStatusCode.Should().BeTrue();
29 | responseContent.Should().Be("V1");
30 | }
31 |
32 | [Test]
33 | public async Task ApiVersioningUrlSegmentReader_WithV2_ShoudPass()
34 | {
35 | // Arrange
36 | var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v2/UrlSegmentTest");
37 |
38 |
39 | // Action
40 | HttpResponseMessage response = await urlSegmentReaderClient.SendAsync(request);
41 | var responseContent = await response.Content.ReadAsStringAsync();
42 |
43 | // Assert
44 | response.IsSuccessStatusCode.Should().BeTrue();
45 | responseContent.Should().Be("V2");
46 | }
47 |
48 | [Test]
49 | public async Task ApiVersioningUrlSegmentReader_WithInvalidVersion_ShoudReturnNotFound()
50 | {
51 | // Arrange
52 | var request = new HttpRequestMessage(HttpMethod.Get, $"/api/UrlSegmentTest");
53 |
54 |
55 | // Action
56 | HttpResponseMessage response = await urlSegmentReaderClient.SendAsync(request);
57 |
58 | // Assert
59 | response.IsSuccessStatusCode.Should().BeFalse();
60 | response.StatusCode.Should().Be(System.Net.HttpStatusCode.NotFound);
61 | }
62 |
63 |
64 | [Test]
65 | public async Task ApiVersioningUrlSegmentReader_WithNoVersion_ShoudReturnBadRequest()
66 | {
67 | // Arrange
68 | var request = new HttpRequestMessage(HttpMethod.Get, "/api/v5/UrlSegmentTest");
69 |
70 | // Action
71 | HttpResponseMessage response = await urlSegmentReaderClient.SendAsync(request);
72 |
73 |
74 | // Assert
75 | response.IsSuccessStatusCode.Should().BeFalse();
76 | response.StatusCode.Should().Be(System.Net.HttpStatusCode.BadRequest);
77 | }
78 |
79 |
80 |
81 | #region Private Methods
82 |
83 | private TestServer GetUrlSegmentReaderTestServer()
84 | {
85 | if (urlSegmentReaderTestServer is not null)
86 | return urlSegmentReaderTestServer;
87 |
88 | var hostBuilder = new WebHostBuilder()
89 | .ConfigureServices(services =>
90 | {
91 | services.AddMvc(i => i.EnableEndpointRouting = false);
92 |
93 | services.AddTechBuddyApiVersioning(config =>
94 | {
95 | config.AssumeDefaultVersionWhenUnspecified = false;
96 |
97 | config.AddUrlSegmentApiVersionReader();
98 | });
99 | })
100 | .Configure(app =>
101 | {
102 | app.UseMvc();
103 | });
104 |
105 | urlSegmentReaderTestServer = new TestServer(hostBuilder);
106 |
107 | return urlSegmentReaderTestServer;
108 | }
109 |
110 | #endregion
111 | }
112 |
--------------------------------------------------------------------------------
/src/ApiVersioningExtension/ApiVersioningExtension.Tests/Usings.cs:
--------------------------------------------------------------------------------
1 | global using NUnit.Framework;
--------------------------------------------------------------------------------
/src/ApiVersioningExtension/ApiVersioningExtension/ApiVersioningExtension.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | disable
7 |
8 | TechBuddy.AspNetCore.ApiVersioning
9 | TechBuddy.Extensions.AspNetCore.ApiVersioning
10 |
11 |
12 |
13 |
14 | true
15 | true
16 | true
17 | TechBuddy.Extensions.AspNetCore.ApiVersioning
18 | ApiVersioning; ApiVersion; Extensions; ApiVersioning Extension;
19 |
20 | LICENSE.txt
21 | README.md
22 |
23 |
24 |
25 | TechBuddyTR Youtube
26 | Salih Cantekin, Community
27 | Provides extension methods for ApiVersioning on your WebAPI project
28 | logo.png
29 | TechBuddyTR Youtube
30 | LICENSE.txt
31 | README.md
32 | https://github.com/TechBuddyTR/TechBuddy.Extensions
33 | https://github.com/TechBuddyTR/TechBuddy.Extensions
34 | git
35 |
36 |
37 |
38 |
39 | Always
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/src/ApiVersioningExtension/ApiVersioningExtension/Extensions/ApiVersioningDependencyInjectionExtension.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Microsoft.AspNetCore.Mvc.Versioning;
3 | using Microsoft.Extensions.DependencyInjection;
4 |
5 | namespace TechBuddy.Extensions.AspNetCore.ApiVersioning;
6 | ///
7 | /// The extensions for ApiVersioning
8 | ///
9 | public static class ApiVersioningDependencyInjectionExtension
10 | {
11 | ///
12 | /// This is used to enabled ApiVersioning in your WebAPI with default configs
13 | ///
14 | /// IServiceCollection
15 | /// returns services
16 | public static IServiceCollection AddTechBuddyApiVersioning(this IServiceCollection services)
17 | {
18 | return services.AddTechBuddyApiVersioning(options =>
19 | {
20 | options.AssumeDefaultVersionWhenUnspecified = true;
21 | options.DefaultApiVersion = "1.0";
22 | options.ReportApiVersions = true;
23 |
24 | options.AddQueryStringApiVersionReader(ApiVersioningConfig.ParameterName);
25 | });
26 | }
27 |
28 | ///
29 | /// This is used to enabled ApiVersioning in your WebAPI
30 | ///
31 | /// IServiceCollection
32 | /// The configuration to customize the api versioning feature
33 | /// services
34 | public static IServiceCollection AddTechBuddyApiVersioning(this IServiceCollection services, Action options)
35 | {
36 | ApiVersioningConfig config = new();
37 | options.Invoke(config);
38 |
39 | AddApiVersioning(services, config);
40 |
41 |
42 | return services;
43 | }
44 |
45 | private static void AddApiVersioning(IServiceCollection services, ApiVersioningConfig config)
46 | {
47 | services.AddApiVersioning(opt =>
48 | {
49 | // Set the error response provider
50 | opt.ErrorResponses = config.ErrorResponseProvider ?? new DefaultErrorResponseProvider();
51 |
52 | // this configuration will allow the api to automaticaly take api_version=1.0 in case it was not specify
53 | opt.AssumeDefaultVersionWhenUnspecified = config.AssumeDefaultVersionWhenUnspecified;
54 | opt.ReportApiVersions = config.ReportApiVersions;
55 |
56 | #region Default Version Parsing
57 |
58 | if (!string.IsNullOrWhiteSpace(config.DefaultApiVersion))
59 | {
60 | if (!ApiVersion.TryParse(config.DefaultApiVersion.Replace("v", ""), out ApiVersion defaultVersion))
61 | throw new ArgumentException($"Not valid version found({config.DefaultApiVersion}). Usage Example: 1.0 or 1 or v1.0 or v1");
62 |
63 | opt.DefaultApiVersion = defaultVersion!;
64 | }
65 |
66 | #endregion
67 |
68 | #region ApiVersionReaders
69 |
70 | if (config.ApiVersioningReaders is { Count: > 0 })
71 | {
72 | var readers = new List();
73 |
74 | foreach (var reader in config.ApiVersioningReaders)
75 | {
76 | IApiVersionReader apiVersionReader = reader.Key switch
77 | {
78 | ApiVersioningReaders.HeaderApiVersionReader => new HeaderApiVersionReader(reader.Value),
79 | ApiVersioningReaders.UrlSegmentApiVersionReader => new UrlSegmentApiVersionReader(),
80 | ApiVersioningReaders.QueryStringApiVersionReader => new QueryStringApiVersionReader(reader.Value),
81 | _ => throw new ArgumentException("Invalid value for reader")
82 | };
83 |
84 | readers.Add(apiVersionReader);
85 | }
86 |
87 | // The way we read the api version
88 | opt.ApiVersionReader = ApiVersionReader.Combine(readers);
89 | }
90 |
91 | #endregion
92 | });
93 | if (config.EnableVersionedApiExplorer)
94 | AddApiVersioningExplorer(services);
95 | }
96 |
97 | private static void AddApiVersioningExplorer(IServiceCollection services)
98 | {
99 | services.AddVersionedApiExplorer(options =>
100 | {
101 | // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service
102 | // note: the specified format code will format the version as "'v'major[.minor][-status]"
103 | options.GroupNameFormat = "'v'VVV";
104 |
105 | // note: this option is only necessary when versioning by url segment. the SubstitutionFormat
106 | // can also be used to control the format of the API version in route templates
107 | options.SubstituteApiVersionInUrl = true;
108 | });
109 | }
110 |
111 | }
112 |
--------------------------------------------------------------------------------
/src/ApiVersioningExtension/ApiVersioningExtension/Infrastructure/ConfigModels/ApiVersioningConfig.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc.Versioning;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Data;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 |
9 | namespace TechBuddy.Extensions.AspNetCore.ApiVersioning;
10 |
11 | ///
12 | /// The config to use when configuring ApiVersioning for your WebAPI projects
13 | ///
14 | public class ApiVersioningConfig
15 | {
16 | internal Dictionary ApiVersioningReaders = new Dictionary();
17 | internal IErrorResponseProvider ErrorResponseProvider { get; set; }
18 | internal const string ParameterName = "x-api-version";
19 |
20 | ///
21 | /// The part of the format for api versioning controller route
22 | ///
23 | public const string ApiVersionFormat = "v{version:apiVersion}";
24 |
25 | ///
26 | /// The format for api versioning controller route
27 | ///
28 | /// [Route()]
29 | public const string ControllerRoute = $"api/{ApiVersionFormat}/[controller]";
30 |
31 | ///
32 | /// The format without api prefix for api versioning controller route
33 | ///
34 | /// [Route()]
35 | public const string ControllerRouteWithoutApi = $"{ApiVersionFormat}/[controller]";
36 |
37 | ///
38 | /// Set true to assume the default version is used when no api version provided
39 | ///
40 | public bool AssumeDefaultVersionWhenUnspecified { get; set; } = true;
41 |
42 | ///
43 | /// The default api version to be used when is true
44 | ///
45 | public string DefaultApiVersion { get; set; } = "1.0";
46 |
47 | ///
48 | /// When true, it reports back the available versions via Response Headers
49 | ///
50 | public bool ReportApiVersions { get; set; } = true;
51 |
52 | ///
53 | /// Enables to be exploered by another App UIs for example Swagger
54 | ///
55 | public bool EnableVersionedApiExplorer { get; set; } = true;
56 |
57 | ///
58 | /// Allows to read the api version from QueryString with a specific parameter name
59 | /// https://localhost/api/GetTest?x-api-version=1.0
60 | ///
61 | /// The parameter name
62 | /// itself
63 | public ApiVersioningConfig AddQueryStringApiVersionReader(string parameterName)
64 | {
65 | ArgumentNullException.ThrowIfNull(parameterName, nameof(parameterName));
66 |
67 | ApiVersioningReaders.TryAdd(ApiVersioning.ApiVersioningReaders.QueryStringApiVersionReader, parameterName);
68 |
69 | return this;
70 | }
71 |
72 | ///
73 | /// Allows to read the api version from route url. Once this is used, must be used in controller route
74 | ///
75 | /// [Route(ApiVersioningConfig.ControllerRoute)]
76 | ///
77 | ///
78 | /// [ApiController]
79 | /// [Route(api/v{version:apiVersion}/[controller])]
80 | /// [ApiVersion("1.0")]
81 | /// public class TestController : ControllerBase
82 | /// {
83 | ///
84 | /// }
85 | ///
86 | ///
87 | ///
88 | /// itself
89 | public ApiVersioningConfig AddUrlSegmentApiVersionReader()
90 | {
91 | ApiVersioningReaders.Add(ApiVersioning.ApiVersioningReaders.UrlSegmentApiVersionReader, "");
92 |
93 | return this;
94 | }
95 |
96 | ///
97 | /// Allows to read the api version from Header
98 | ///
99 | /// The header key name
100 | /// itself
101 | public ApiVersioningConfig AddHeaderApiVersionReader(string headerKey)
102 | {
103 | ArgumentNullException.ThrowIfNull(headerKey);
104 |
105 | ApiVersioningReaders.Add(ApiVersioning.ApiVersioningReaders.HeaderApiVersionReader, headerKey);
106 |
107 | return this;
108 | }
109 |
110 | ///
111 | /// Adds the provided ErrorResponseProvider as custom provider to be used when any Api Versioning exception is occured
112 | ///
113 | /// The provider derived from
114 | /// itself
115 | public ApiVersioningConfig UseErrorResponseProvider(T errorResponseProvider) where T : IErrorResponseProvider
116 | {
117 | ErrorResponseProvider = errorResponseProvider;
118 |
119 | return this;
120 | }
121 |
122 | ///
123 | /// Adds the ErrorResponseProvider by creating an instance of T to be used when any Api Versioning exception is occured
124 | ///
125 | /// The provider derived from
126 | /// itself
127 | public ApiVersioningConfig UseErrorResponseProvider() where T : IErrorResponseProvider
128 | {
129 | return UseErrorResponseProvider(Activator.CreateInstance());
130 | }
131 | }
132 |
133 | ///
134 | /// The enum to provide ApiVersionReader method
135 | ///
136 | internal enum ApiVersioningReaders
137 | {
138 | ///
139 | /// Read from Header
140 | ///
141 | HeaderApiVersionReader = 1,
142 |
143 | ///
144 | /// Read from Url Route
145 | ///
146 | UrlSegmentApiVersionReader = 2,
147 |
148 | ///
149 | /// Read from Query String
150 | ///
151 | QueryStringApiVersionReader = 4
152 | }
153 |
--------------------------------------------------------------------------------
/src/ApiVersioningExtension/ApiVersioningExtension/licenses/license.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 [Salih Cantekin]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/ApiVersioningExtension/ApiVersioningExtension/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TechBuddyTR/TechBuddy.Extensions/df08c47466a9a5cf5d5bd472865743f2a6c2b8ae/src/ApiVersioningExtension/ApiVersioningExtension/logo.png
--------------------------------------------------------------------------------
/src/ApiVersioningExtension/ApiVersioningExtension/readme.md:
--------------------------------------------------------------------------------
1 | # ApiVersioning
2 |
3 | ApiVersioning Extension is aiming to add ApiVersioning Feature for your WebAPI Projects. It's configurable as well as it is configured with default values once this is implemented.
4 |
5 |
6 | ApiVersioningConfig has three different functions, can be seen below, which is to add the ability to the system to read the provided api-version from different places.
7 |
8 |
9 | ## Nuget
10 | | Package Name | Package | Download |
11 | | ------------- | ------------- | ------------- |
12 | | ApiVersioning | [](https://www.nuget.org/packages/TechBuddy.Extensions.AspNetCore.ApiVersioning) | [](https://www.nuget.org/packages/TechBuddy.Extensions.AspNetCore.ApiVersioning/) |
13 |
14 | ### Installation
15 |
16 | ```bash
17 | PM> NuGet\Install-Package TechBuddy.Extensions.AspNetCore.ApiVersioning
18 | ```
19 | or
20 | ```bash
21 | dotnet add package TechBuddy.Extensions.AspNetCore.ApiVersioning
22 | ```
23 |
24 | ----
25 |
26 | ## Usage
27 |
28 | ```csharp
29 | builder.Services.AddTechBuddyApiVersioning(config =>
30 | {
31 | config.AssumeDefaultVersionWhenUnspecified = true; // To assume default version number will be used when it is not specified
32 | config.DefaultApiVersion = "1.0"; // Default Api Version
33 | config.EnableVersionedApiExplorer = true; // Allows it to be enabled in OpenAPI Supports such as Swagger. It returns back all the available api versions in your WebAPI project
34 | config.ReportApiVersions = true; // Allows all the available api versions for the specific endpoint to be returned in Response Header section with api-supported-versions key.
35 |
36 | config.AddUrlSegmentApiVersionReader(); // Reads from Url Route -> https://localhost/api/v1/Users?id=5
37 | config.AddHeaderApiVersionReader("x-api-version"); // Reads from Header with provided key (x-api-version)
38 | config.AddQueryStringApiVersionReader("x-api-version"); // Reads from query string with provided key (x-api-version) -> https://localhost/api/v1/Users?x-api-version=1.0&id=5
39 | });
40 | ```
41 |
42 | It could be used like showed below. In this case, version 1.0 will be used as default version when not specified. And also ReportApiVersions will be true.
43 | ```csharp
44 | builder.Services.AddTechBuddyApiVersioning();
45 | ```
46 |
47 |
48 | If you injected ApiVersioning with `UrlSegmentReader`, your controller must have the `Route` parameter accordingly.
49 | However, if you use more than one reader and at least one of them is UrlSegmentReader (for instance; UrlSegment and Header), your controllers must have both route parameter with and without `v{version:apiVersion}`
50 |
51 | ```csharp
52 | [ApiController]
53 | [Route(ApiVersioningConfig.ControllerRoute)] // api/v{version:apiVersion}/[controller] -> this for UrlSegment
54 | //[Route("api/[controller]")] -> this is for the others (Header, QueryString)
55 | [ApiVersion("2.0")]
56 | public class TestController : ControllerBase
57 | {
58 | [HttpGet("GetTest")]
59 | public ActionResult Get()
60 | {
61 | return Ok("Test Success V2");
62 | }
63 | }
64 | ```
65 |
66 | ----
67 |
68 | ## Usage with CustomErrorMessageResponseProvider
69 |
70 | We are able to add multiple ApiVersionReader together. However, if the api version we provided is different than we provided on the other, such as v1 on QueryString but v2 on header, there will be an exception thrown.
71 | When this exception is occured, the default response model will be provided by .Net ApiVersioning middleware. Nonetheless, we are also able to customize this response model.
72 |
73 | To do that;
74 |
75 | ```csharp
76 |
77 | internal class CustomErrorResponseProvider : IErrorResponseProvider
78 | {
79 | public IActionResult CreateResponse(ErrorResponseContext context)
80 | {
81 | var response = new()
82 | {
83 | ErrorCode = context.ErrorCode,
84 | ErrorMessage = "ApiVersioning Exception",
85 | ErrorDetail = context.Message
86 | };
87 |
88 | var result = new ObjectResult(response)
89 | {
90 | StatusCode = StatusCodes.Status400BadRequest
91 | };
92 |
93 | return result;
94 | }
95 | }
96 |
97 | builder.Services.AddTechBuddyApiVersioning(config =>
98 | {
99 | config.AddHeaderApiVersionReader("x-api-version"); // Reads from Header with provided key (x-api-version)
100 | config.AddQueryStringApiVersionReader("x-api-version"); // Reads from query string with provided key (x-api-version) -> https://localhost/api/v1/Users?x-api-version=1.0&id=5
101 |
102 | config.UseErrorResponseProvider(new CustomErrorResponseProvider());
103 | // or
104 | //config.UseErrorResponseProvider();
105 | });
106 | ```
107 |
--------------------------------------------------------------------------------
/src/AspNetCoreExtensions.Tests.Common/AspNetCoreExtensions.Tests.Common/AspNetCoreExtensions.Tests.Common.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 |
7 | false
8 | TechBuddy.Tests.Common
9 | TechBuddy.Extensions.Tests.Common
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/AspNetCoreExtensions.Tests.Common/AspNetCoreExtensions.Tests.Common/TestCommon/Builders/HttpRequestBuilder.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using Microsoft.Extensions.Primitives;
3 | using System.Text;
4 | using System.Text.Json;
5 | using TechBuddy.Extensions.Tests.Common.TestCommon.Constants;
6 |
7 | namespace TechBuddy.Extensions.Tests.Common.TestCommon.Builders;
8 |
9 | ///
10 | /// The HttpRequestBuilder to build httprequest for testing purpose
11 | ///
12 | public class HttpRequestBuilder
13 | {
14 | private readonly DefaultHttpContext context;
15 | private readonly HttpRequest request;
16 | private readonly Dictionary> headerCollectionBuilderDictionary;
17 | private HttpMethod httpMethod = HttpMethod.Get;
18 | private string requestUrl;
19 | private string bodyJson;
20 |
21 | ///
22 | /// Create a new builder
23 | ///
24 | /// new HttpRequestBuilder
25 | public static HttpRequestBuilder CreateRequest(string url = "", HttpMethod method = null)
26 | {
27 | var builder = new HttpRequestBuilder
28 | {
29 | httpMethod = method,
30 | requestUrl = url
31 | };
32 |
33 | return builder;
34 | }
35 |
36 | ///
37 | /// Instantiates a new instance - internal to promote static CreateRequest
38 | ///
39 | internal HttpRequestBuilder()
40 | {
41 | context = new DefaultHttpContext();
42 | request = context.Request;
43 | headerCollectionBuilderDictionary = new Dictionary>();
44 | }
45 |
46 | ///
47 | /// Add a value or set of values to the header collection against the specified key
48 | ///
49 | /// Header key
50 | /// Values for key
51 | /// HttpRequestBuilder
52 | public HttpRequestBuilder AddHeader(string key, params string[] value)
53 | {
54 | if (headerCollectionBuilderDictionary.ContainsKey(key))
55 | {
56 | headerCollectionBuilderDictionary[key].AddRange(value);
57 | }
58 | else
59 | {
60 | headerCollectionBuilderDictionary.Add(key, new List(value));
61 | }
62 |
63 | return this;
64 | }
65 |
66 | ///
67 | /// Sets the body of the request to a stream containing the given text
68 | ///
69 | /// Body string content
70 | /// Http Request Builder
71 | public HttpRequestBuilder SetBody(string stringBodyContent)
72 | {
73 | bodyJson = stringBodyContent;
74 |
75 | var ms = new MemoryStream();
76 | using (var writer = new StreamWriter(ms, Encoding.Default, 4096, true))
77 | {
78 | writer.Write(stringBodyContent);
79 | }
80 |
81 | ms.Seek(0, SeekOrigin.Begin);
82 | request.Body = ms;
83 |
84 | return this;
85 | }
86 |
87 | ///
88 | /// Sets the body of the request to a stream containing the given object
89 | ///
90 | /// The type of the object
91 | /// The object to serialize
92 | /// Http Request Builder
93 | public HttpRequestBuilder SetBody(T obj) where T : new()
94 | {
95 | var objJson = JsonSerializer.Serialize(obj, GeneralConstants.JsonOptions);
96 |
97 | return SetBody(objJson);
98 | }
99 |
100 | ///
101 | /// Return the HttpRequest from the builder actions
102 | ///
103 | ///
104 | public HttpRequest ToRequest()
105 | {
106 | if (headerCollectionBuilderDictionary.Any())
107 | {
108 | foreach (var header in headerCollectionBuilderDictionary)
109 | {
110 | request.Headers.Add(header.Key, new StringValues(header.Value.ToArray()));
111 | }
112 | }
113 |
114 | request.Method = httpMethod?.ToString();
115 |
116 | return request;
117 | }
118 |
119 | public HttpContent ToHttpContent()
120 | {
121 | return new StreamContent(request.Body);
122 | }
123 |
124 | public HttpRequestMessage ToRequestMessage()
125 | {
126 | var message = new HttpRequestMessage(httpMethod, requestUrl)
127 | {
128 | Content = new StringContent(bodyJson, Encoding.UTF8, "application/json"),
129 | RequestUri = new Uri(requestUrl, UriKind.RelativeOrAbsolute)
130 | };
131 |
132 | return message;
133 | }
134 | }
--------------------------------------------------------------------------------
/src/AspNetCoreExtensions.Tests.Common/AspNetCoreExtensions.Tests.Common/TestCommon/Constants/GeneralConstants.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 |
3 | namespace TechBuddy.Extensions.Tests.Common.TestCommon.Constants;
4 | public sealed class GeneralConstants
5 | {
6 | public static JsonSerializerOptions JsonOptions { get; } = new()
7 | {
8 | PropertyNameCaseInsensitive = true,
9 | WriteIndented = true
10 | };
11 | }
12 |
--------------------------------------------------------------------------------
/src/AspNetCoreExtensions.Tests.Common/AspNetCoreExtensions.Tests.Common/TestCommon/Constants/TestConstants.cs:
--------------------------------------------------------------------------------
1 | namespace TechBuddy.Extensions.Tests.Common.TestCommon.Constants;
2 | public sealed class TestConstants
3 | {
4 | public const string ApiversionKey = "x-api-version";
5 |
6 | public const string ContentType = "Content-Type";
7 |
8 |
9 | public const string ExceptionMessage = "Exception is thrown intentionally";
10 | public const string DefaultExceptionMessage = "Internal server error occured!";
11 | public const string ControllerRoute = "api/v{version:apiVersion}/[controller]";
12 |
13 |
14 | }
--------------------------------------------------------------------------------
/src/ExceptionHandlingExtension/ExceptionHandlingExtension.Tests/ExceptionHandlingExtension.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/ExceptionHandlingExtension/ExceptionHandlingExtension.Tests/Infrastructure/Controllers/TestController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using TechBuddy.Extensions.Tests.Common.TestCommon.Constants;
3 |
4 | namespace ExceptionHandlingExtension.Tests.Infrastructure.Controllers;
5 |
6 | [ApiController]
7 | [Route("api/[controller]")]
8 | [ApiVersion("1.0")]
9 | public class TestController : ControllerBase
10 | {
11 | [HttpGet()]
12 | public ActionResult Get()
13 | {
14 | return Ok("V1");
15 | }
16 |
17 | [HttpGet("ThrowException")]
18 | public ActionResult ThrowException()
19 | {
20 | throw new Exception(TestConstants.ExceptionMessage);
21 | }
22 |
23 | [HttpGet("ThrowCustomException")]
24 | public ActionResult ThrowCustomException()
25 | {
26 | throw new InvalidOperationException();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/ExceptionHandlingExtension/ExceptionHandlingExtension.Tests/Infrastructure/Extensions/HttpContextExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using TechBuddy.Extensions.Tests.Common.TestCommon.Constants;
3 | using System.Net;
4 | using System.Text;
5 | using System.Text.Json;
6 |
7 | namespace ExceptionHandlingExtension.Tests.Infrastructure.Extensions;
8 | internal static class HttpContextExtensions
9 | {
10 | internal static async Task WriteResponseAsync(this HttpContext context, object resultObj, HttpStatusCode statusCode)
11 | {
12 | context.Response.ContentType = TestConstants.ContentType;
13 | context.Response.StatusCode = (int)statusCode;
14 |
15 | var json = JsonSerializer.Serialize(resultObj, GeneralConstants.JsonOptions);
16 | await context.Response.WriteAsync(json, Encoding.UTF8);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/ExceptionHandlingExtension/ExceptionHandlingExtension.Tests/Usings.cs:
--------------------------------------------------------------------------------
1 | global using NUnit.Framework;
--------------------------------------------------------------------------------
/src/ExceptionHandlingExtension/ExceptionHandlingExtension/ExceptionHandlingExtension.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 |
7 | TechBuddy.AspNetCore.ExceptionHandling
8 | TechBuddy.Extensions.AspNetCore.ExceptionHandling
9 |
10 |
11 |
12 |
13 | true
14 | true
15 | true
16 | TechBuddy.Extensions.AspNetCore.ExceptionHandling
17 | ExceptionHandling; Exception; Handling; ExceptionHandling Extensions; Exception Extensions;
18 |
19 |
20 |
21 | TechBuddyTR Youtube
22 | Salih Cantekin, Community
23 | Provides extension methods for ExceptionHandling on your WebAPI project
24 | logo.png
25 | TechBuddyTR Youtube
26 | LICENSE.txt
27 | README.md
28 | https://github.com/TechBuddyTR/TechBuddy.Extensions
29 | https://github.com/TechBuddyTR/TechBuddy.Extensions
30 | git
31 |
32 |
33 |
34 |
35 | Always
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/ExceptionHandlingExtension/ExceptionHandlingExtension/Extensions/ExceptionHandlingDependencyInjectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.AspNetCore.Diagnostics;
3 | using Microsoft.AspNetCore.Http;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Microsoft.Extensions.Logging;
6 | using TechBuddy.Extensions.AspNetCore.ExceptionHandling.Infrastructure.ExceptionHandlers;
7 |
8 | namespace TechBuddy.Extensions.AspNetCore.ExceptionHandling;
9 |
10 | ///
11 | /// The ExceptionHandling extension methods for WebApi projects
12 | ///
13 | public static class ExceptionHandlingDependencyInjectionExtensions
14 | {
15 | ///
16 | /// Configures the ExceptionHandling Middleware with default opt.
17 | /// With default opt, Logging with details will be enabled
18 | ///
19 | /// The IApplicationBuilder
20 | /// The IApplicationBuilder
21 | public static Task ConfigureTechBuddyExceptionHandling(this IApplicationBuilder app)
22 | {
23 | return app.ConfigureTechBuddyExceptionHandling(options =>
24 | {
25 | options.UseLogger();
26 | });
27 | }
28 |
29 | ///
30 | /// Configures the ExceptionHandling Middleware with provided opt.
31 | ///
32 | /// The IApplicationBuilder
33 | /// The ExceptionHandlingConfig Action
34 | /// The IApplicationBuilder
35 | public static Task ConfigureTechBuddyExceptionHandling(this IApplicationBuilder app,
36 | Action optionsAction)
37 | {
38 | ExceptionHandlingOptions opt = new();
39 | optionsAction(opt); // Fill the options
40 |
41 | return ConfigureTechBuddyExceptionHandling(app, opt);
42 | }
43 |
44 | ///
45 | /// Configures the ExceptionHandling Middleware with provided opt.
46 | ///
47 | /// The IApplicationBuilder
48 | /// The ExceptionHandlingConfig
49 | /// The IApplicationBuilder
50 | public static async Task ConfigureTechBuddyExceptionHandling(this IApplicationBuilder app,
51 | ExceptionHandlingOptions opt)
52 | {
53 | ILogger logger = opt.Logger;
54 |
55 | if (logger is null && opt.LoggingEnabled)
56 | {
57 | logger = GetLoggerService(app.ApplicationServices);
58 | }
59 |
60 | app.UseExceptionHandler(async options =>
61 | {
62 | if (opt.ExceptionHandler is not null)
63 | {
64 | await RegisterHandlers(options,
65 | logger,
66 | opt.ExceptionHandler,
67 | opt);
68 | }
69 | else
70 | {
71 | await RegisterHandlers(options,
72 | logger,
73 | (c, e, l) => DefaultExceptionHandler.Handle(c, e, l, opt.UseExceptionDetails),
74 | opt);
75 | }
76 | });
77 |
78 | await Task.CompletedTask;
79 | }
80 |
81 |
82 | private static ILogger GetLoggerService(IServiceProvider sp)
83 | {
84 | // Try to get ILogger first
85 | var serviceLogger = sp.GetService();
86 |
87 | if (serviceLogger is not null)
88 | return serviceLogger;
89 |
90 | // If no ILogger, try to create yours by using ILoggerFactory
91 | var logFactory = sp.GetService();
92 | serviceLogger = logFactory.CreateLogger();
93 |
94 | return serviceLogger;
95 | }
96 |
97 | private static async Task RegisterHandlers(IApplicationBuilder app,
98 | ILogger logger,
99 | Func exceptionHandler,
100 | ExceptionHandlingOptions options)
101 | {
102 | app.Run(async context =>
103 | {
104 | var exceptionObject = context.Features.Get();
105 | var type = exceptionObject.Error.GetType();
106 | options.ExceptionHandlersDictionary.TryGetValue(type, out var handler);
107 |
108 | if (handler is not null)
109 | {
110 | await handler.Invoke(context, exceptionObject.Error, logger);
111 | }
112 | else
113 | {
114 | await exceptionHandler.Invoke(context, exceptionObject.Error, logger);
115 | }
116 | });
117 |
118 | await Task.CompletedTask;
119 | }
120 |
121 |
122 |
123 | }
124 |
--------------------------------------------------------------------------------
/src/ExceptionHandlingExtension/ExceptionHandlingExtension/Infrastructure/ExceptionHandlers/DefaultExceptionHandler.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using Microsoft.Extensions.Logging;
3 | using TechBuddy.Extensions.AspNetCore.ExceptionHandling.Infrastructure.Extensions;
4 | using TechBuddy.Extensions.AspNetCore.ExceptionHandling.Infrastructure.Models;
5 |
6 | namespace TechBuddy.Extensions.AspNetCore.ExceptionHandling.Infrastructure.ExceptionHandlers;
7 | internal class DefaultExceptionHandler
8 | {
9 | internal static async Task Handle(HttpContext context,
10 | Exception exception,
11 | ILogger logger,
12 | bool useExceptionDetails = false)
13 | {
14 |
15 | var res = new DefaultExceptionHandlerResponseModel()
16 | {
17 | StatusCode = System.Net.HttpStatusCode.InternalServerError,
18 | Detail = useExceptionDetails
19 | ? exception.ToString()
20 | : ExceptionHandlingConstants.DefaultExceptionMessage
21 | };
22 |
23 | logger?.LogError(exception, exception.ToString());
24 | await context.WriteResponseAsync(res, res.StatusCode);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/ExceptionHandlingExtension/ExceptionHandlingExtension/Infrastructure/ExceptionHandlingConstants.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 |
3 | namespace TechBuddy.Extensions.AspNetCore.ExceptionHandling.Infrastructure;
4 | internal class ExceptionHandlingConstants
5 | {
6 | internal const string DefaultExceptionMessage = "Internal server error occured!";
7 |
8 | internal const string DefaultResponseContentType = "application/json";
9 |
10 | internal static JsonSerializerOptions JsonOptions = new()
11 | {
12 | PropertyNameCaseInsensitive = true,
13 | WriteIndented = true
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/src/ExceptionHandlingExtension/ExceptionHandlingExtension/Infrastructure/Extensions/HttpContextExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using System.Net;
3 | using System.Text;
4 | using System.Text.Json;
5 | using TechBuddy.Extensions.AspNetCore.ExceptionHandling.Infrastructure;
6 |
7 | namespace TechBuddy.Extensions.AspNetCore.ExceptionHandling.Infrastructure.Extensions;
8 | internal static class HttpContextExtensions
9 | {
10 | internal static async Task WriteResponseAsync(this HttpContext context, object resultObj, HttpStatusCode statusCode)
11 | {
12 | context.Response.ContentType = ExceptionHandlingConstants.DefaultResponseContentType;
13 | context.Response.StatusCode = (int)statusCode;
14 |
15 | var json = JsonSerializer.Serialize(resultObj, ExceptionHandlingConstants.JsonOptions);
16 | await context.Response.WriteAsync(json, Encoding.UTF8);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/ExceptionHandlingExtension/ExceptionHandlingExtension/Infrastructure/Models/DefaultExceptionHandlerResponseModel.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 |
3 | namespace TechBuddy.Extensions.AspNetCore.ExceptionHandling.Infrastructure.Models;
4 |
5 | ///
6 | /// The model to be used in when Exception handled but handler is not customized
7 | ///
8 | public class DefaultExceptionHandlerResponseModel
9 | {
10 | ///
11 | /// The error detail
12 | ///
13 | public string Detail { get; set; }
14 |
15 | ///
16 | /// The http status code
17 | ///
18 | public HttpStatusCode StatusCode { get; set; }
19 |
20 | ///
21 | /// The parameterless constructor
22 | ///
23 | public DefaultExceptionHandlerResponseModel()
24 | {
25 | StatusCode = HttpStatusCode.InternalServerError;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/ExceptionHandlingExtension/ExceptionHandlingExtension/Infrastructure/Models/OptionsModels/ExceptionHandlingOptions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using Microsoft.Extensions.Logging;
3 |
4 | namespace TechBuddy.Extensions.AspNetCore.ExceptionHandling;
5 |
6 | ///
7 | /// The options model for ExceptionHandling
8 | ///
9 |
10 | public class ExceptionHandlingOptions
11 | {
12 | internal Dictionary> ExceptionHandlersDictionary;
13 | internal Func ExceptionHandler;
14 | internal ILogger Logger { get; set; }
15 |
16 | ///
17 | /// The parameterless constructor
18 | ///
19 | public ExceptionHandlingOptions()
20 | {
21 | ExceptionHandlersDictionary = new Dictionary>();
22 | }
23 |
24 | ///
25 | /// Gets whether the logging is enabled when either or is called
26 | ///
27 | public bool LoggingEnabled { get; internal set; } = false;
28 |
29 | ///
30 | /// Gets or sets whether the stack trace of the exception will be present in the response model
31 | ///
32 | public bool UseExceptionDetails { get; set; }
33 |
34 | ///
35 | /// Sets the provided as the logger in the default exception handler
36 | ///
37 | /// The type of logger
38 | /// The logger to be used in default exception handler
39 | public void UseLogger(T logger) where T : ILogger
40 | {
41 | Logger = logger;
42 | UseLogger();
43 | }
44 |
45 | ///
46 | /// Enables the default ILogger for the default exception handler
47 | ///
48 | public void UseLogger()
49 | {
50 | LoggingEnabled = true;
51 | }
52 |
53 | ///
54 | /// Sets the provided as custom handler.
55 | /// This functions will be called when any unhandled exception is occured
56 | ///
57 | /// The exception handler function
58 | public void UseCustomHandler(Func exceptionHandler)
59 | {
60 | ExceptionHandler = exceptionHandler;
61 | }
62 |
63 | ///
64 | /// Adds the provided handler for a specific exception types. For instance, you may want to handle differently when is occured
65 | ///
66 | /// The specific exception
67 | /// The custom handler
68 | /// Thrown when duplicate exception is added
69 | public void AddCustomHandler(Func handler) where TException : Exception
70 | {
71 | var added = ExceptionHandlersDictionary.TryAdd(typeof(TException), handler);
72 |
73 | if (!added)
74 | throw new InvalidOperationException($"{typeof(TException)} is already added for another handler");
75 | }
76 |
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/src/ExceptionHandlingExtension/ExceptionHandlingExtension/licenses/license.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 [Salih Cantekin]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/ExceptionHandlingExtension/ExceptionHandlingExtension/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TechBuddyTR/TechBuddy.Extensions/df08c47466a9a5cf5d5bd472865743f2a6c2b8ae/src/ExceptionHandlingExtension/ExceptionHandlingExtension/logo.png
--------------------------------------------------------------------------------
/src/ExceptionHandlingExtension/ExceptionHandlingExtension/readme.md:
--------------------------------------------------------------------------------
1 | # Exception Handling
2 |
3 | Exception Handling is giving you the opportunity to manage all *unhandled* exceptions in a single point. By implementing this extension in your WebAPI project,
4 | you'll be able to catch all unhandled exceptions and return back a standard exception model as well as you'll be able to provide your own handler method which will be called once any unhandled exception is occured.
5 |
6 | Moreover, you can provide different handler for different `Exception` types. For instance, HandlerX for `ValidationException` or HandlerY for `UnAuthorizationException`, and HandlerZ for the rest
7 |
8 | Apart from that, you can customize logging by providing an ILogger interface to log the details once the exception is cought by the handler.
9 | `UseExceptionDetails` is important because once the default handler is used, it'll return back the exception details.
10 | If `UseExceptionDetails` is true, it returns the StackTrace of the Exception.
11 | When it is false, however, it returns the single message ("Internal Server Error Occured!") and 500 StatusCode. So you can use this details in your Dev or PreProd environment but not on production.
12 | On the other hand, logging the details is irrelevant to this parameter. No matter it is true or false, it logs everything with StackTrace.
13 |
14 | ## Nuget
15 | | Package Name | Package | Download |
16 | | ------------- | ------------- | ------------- |
17 | | ExceptionHandling | [](https://www.nuget.org/packages/TechBuddy.Extensions.AspNetCore.ExceptionHandling) | [](https://www.nuget.org/packages/TechBuddy.Extensions.AspNetCore.ExceptionHandling/) |
18 |
19 | ### Installation
20 |
21 | ```bash
22 | PM> NuGet\Install-Package TechBuddy.Extensions.AspNetCore.ExceptionHandling
23 | ```
24 | or
25 | ```bash
26 | dotnet add package TechBuddy.Extensions.AspNetCore.ExceptionHandling
27 | ```
28 |
29 | ----
30 |
31 |
32 | ### Usage 1
33 |
34 | ```csharp
35 |
36 | app.ConfigureTechBuddyExceptionHandling(opt =>
37 | {
38 | var logger = app.Services.GetService();
39 | opt.UseExceptionDetails = true; // details will be in the default response model (`DefaultExceptionHandlerResponseModel`)
40 | opt.UseLogger(logger); // details will be logged not matter `UseExceptionDetails` is true or false
41 |
42 | // If you use logger but provide no logger, it will create its own logger
43 | // opt.UseLogger();
44 | });
45 |
46 | ```
47 |
48 | ### Usage 2
49 |
50 | In this usage, you must provide a handler function which will be invoked when the exception is occured. From now on, all the responsibility is on you.
51 |
52 | ```csharp
53 | opt.UseCustomHandler((httpContext, exception, logger) =>
54 | {
55 | logger.LogError("Unhandled exception occured");
56 | var dynamicResponseModel = new { ErrorMessage = exception.Message };
57 |
58 | // we can set the response but don't have to
59 | return httpContext.Response.WriteAsJsonAsync(dynamicResponseModel); // WriteAsJsonAsync is an extension method.
60 | });
61 | ```
62 |
63 | ### Usage 3
64 |
65 | In this usage, UseExceptionDetails is set to true and also default ILogger is used if it's enabled in the system (app.ApplicationServices.GetService())
66 |
67 | ```csharp
68 |
69 | app.ConfigureTechBuddyExceptionHandling();
70 |
71 | ```
72 |
73 |
74 | ### Usage 4
75 |
76 | In this usage, you are also able to use different handlers for different type of exceptions.
77 |
78 | ```csharp
79 |
80 | var options = new ExceptionHandlingOptions();
81 |
82 | options.AddCustomHandler((context, ex, logger) =>
83 | {
84 | logger.LogError("Unhandled exception occured");
85 | var dynamicResponseModel = new { ErrorMessage = ex.Message, Type = "ValidationException" };
86 |
87 | // we can set the response but don't have to
88 | return httpContext.Response.WriteAsJsonAsync(dynamicResponseModel);
89 | });
90 |
91 |
92 | options.AddCustomHandler((context, ex, logger) =>
93 | {
94 | logger.LogError("ArgumentNullException occured");
95 | var dynamicResponseModel = new { ErrorMessage = ex.Message, Type = "ArgumentNullException" };
96 |
97 | // we can set the response but don't have to
98 | return httpContext.Response.WriteAsJsonAsync(dynamicResponseModel);
99 | });
100 |
101 |
102 | // All the other exceptions
103 | opt.UseCustomHandler(async (context, ex, logger) =>
104 | {
105 | logger.LogError("Unhandled exception occured");
106 | var dynamicResponseModel = new { ErrorMessage = ex.Message, Type = "Exception" };
107 |
108 | // we can set the response but don't have to
109 | return httpContext.Response.WriteAsJsonAsync(dynamicResponseModel);
110 | });
111 |
112 | app.ConfigureTechBuddyExceptionHandling(options);
113 |
114 | ```
115 |
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension.Tests.WebApi/Controllers/v1/TestController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 |
3 | namespace SwaggerExtension.Tests.WebApi.Controllers.v1;
4 |
5 | [Route("api/[controller]")]
6 | [ApiController]
7 | [ApiVersion("1")]
8 | public class TestController : ControllerBase
9 | {
10 | ///
11 | /// Gets OK1 result
12 | ///
13 | /// IActionResult
14 | [HttpGet]
15 | public IActionResult Get()
16 | {
17 | return Ok("OK1");
18 | }
19 |
20 | ///
21 | /// Gets the FullName property in the post model
22 | ///
23 | ///
24 | /// IActionResult
25 | [HttpPost]
26 | //[ProducesResponseType(StatusCodes.Status400BadRequest, typeof(BadRequestResponseModel))]
27 | public ActionResult Post(TestModel testModel)
28 | {
29 | if (testModel.Id == 0)
30 | return BadRequest("Error!");
31 |
32 | return Ok(testModel);
33 | }
34 | }
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension.Tests.WebApi/Controllers/v2/TestControllerV2.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 |
3 | namespace SwaggerExtension.Tests.WebApi.Controllers.v2;
4 |
5 | [Route("api/[controller]")]
6 | [ApiController]
7 | [ApiVersion("2")]
8 | public class TestControllerV2 : ControllerBase
9 | {
10 | [HttpGet]
11 | public IActionResult Get()
12 | {
13 | return Ok("OK2");
14 | }
15 | }
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension.Tests.WebApi/Infrastructure/Models/BadRequestResponseModel.cs:
--------------------------------------------------------------------------------
1 | namespace SwaggerExtension.Tests.WebApi.Infrastructure.Models;
2 |
3 | public class BadRequestResponseModel
4 | {
5 | public string ErrorDetail { get; set; }
6 | }
7 |
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension.Tests.WebApi/Infrastructure/Models/TestModel.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | public class TestModel
4 | {
5 | public int Id { get; set; }
6 |
7 | public string FullName { get; set; }
8 |
9 | [JsonIgnore]
10 | public int Age { get; set; }
11 | }
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension.Tests.WebApi/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using SwaggerExtension.Tests.WebApi.Controllers.v2;
3 | using SwaggerExtension.Tests.WebApi.Infrastructure.Models;
4 | using TechBuddy.Extensions.AspNetCore.ApiVersioning;
5 | using TechBuddy.Extensions.OpenApi;
6 |
7 | var builder = WebApplication.CreateBuilder(args);
8 |
9 |
10 | builder.Services.AddControllers();
11 | builder.Services.AddEndpointsApiExplorer();
12 |
13 | //builder.Services.AddApiVersioning();
14 | builder.Services.AddTechBuddyApiVersioning(option =>
15 | {
16 | option.AddQueryStringApiVersionReader("x-api-version");
17 | option.EnableVersionedApiExplorer = true;
18 | });
19 |
20 | builder.Services.ConfigureTechBuddySwagger(config =>
21 | {
22 | config.AddHeader("TenantId", "TB_Company");
23 |
24 | config.BearerConfig = new SwaggerBearerConfig()
25 | {
26 | AuthEnabled = true
27 | };
28 |
29 | config.XmlDocConfig = new SwaggerDocConfig()
30 | {
31 | XmlFilePath = "SwaggerExtension.Tests.WebApi.xml"
32 | };
33 |
34 |
35 | var provider = ResponseTypeModelProviderConfig.CreateDefault();
36 | provider.ClearDefaultTypes();
37 | provider.ClearDefaultResponseHttpStatusCodes();
38 |
39 | provider
40 | .AddDefaultType(typeof(Task), typeof(ActionResult), typeof(IActionResult))
41 | .AddDefaultResponseHttpStatusCodeForAll(System.Net.HttpStatusCode.OK)
42 | .AddSpecificTypeForSpecificHttpStatusCode(System.Net.HttpStatusCode.OK, typeof(string))
43 |
44 | .AddDefaultResponseHttpStatusCodeForHttpMethods(HttpMethod.Post, System.Net.HttpStatusCode.BadRequest)
45 | .AddSpecificTypeForSpecificHttpStatusCode(System.Net.HttpStatusCode.BadRequest, typeof(BadRequestResponseModel))
46 | .AddDefaultResponseHttpStatusCodeForHttpMethods(HttpMethod.Get, System.Net.HttpStatusCode.NoContent)
47 |
48 | .ExcludeController(nameof(TestControllerV2));
49 |
50 | config.ResponseTypeModelProviderConfig = provider;
51 | });
52 |
53 | var app = builder.Build();
54 |
55 | if (app.Environment.IsDevelopment())
56 | app.UseTechBuddySwagger();
57 |
58 |
59 | app.UseHttpsRedirection();
60 |
61 | app.UseAuthorization();
62 |
63 | app.MapControllers();
64 |
65 | app.Run();
66 |
67 |
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension.Tests.WebApi/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:44501",
8 | "sslPort": 44374
9 | }
10 | },
11 | "profiles": {
12 | "SwaggerExtension.Tests.WebApi": {
13 | "commandName": "Project",
14 | "dotnetRunMessages": true,
15 | "launchBrowser": true,
16 | "launchUrl": "swagger",
17 | "applicationUrl": "https://localhost:7167;http://localhost:5140",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development"
20 | }
21 | },
22 | "IIS Express": {
23 | "commandName": "IISExpress",
24 | "launchBrowser": true,
25 | "launchUrl": "swagger",
26 | "environmentVariables": {
27 | "ASPNETCORE_ENVIRONMENT": "Development"
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension.Tests.WebApi/SwaggerExtension.Tests.WebApi.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 |
7 | SwaggerExtension.Tests.WebApi.xml
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension.Tests.WebApi/SwaggerExtension.Tests.WebApi.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | SwaggerExtension.Tests.WebApi
5 |
6 |
7 |
8 |
9 | Gets OK1 result
10 |
11 | IActionResult
12 |
13 |
14 |
15 | Gets the FullName property in the post model
16 |
17 |
18 | IActionResult
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension.Tests.WebApi/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension.Tests.WebApi/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "AllowedHosts": "*"
9 | }
10 |
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension.Tests/Helpers/DummyController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 |
3 | namespace SwaggerExtension.Tests.Helpers;
4 | public sealed class DummyController : ControllerBase
5 | {
6 | [HttpGet]
7 | public void DummyAction()
8 | {
9 |
10 | }
11 | }
12 |
13 |
14 | public sealed class DummyController2 : ControllerBase
15 | {
16 | [HttpGet]
17 | public void DummyAction2()
18 | {
19 |
20 | }
21 | }
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension.Tests/SwaggerConfigTests.cs:
--------------------------------------------------------------------------------
1 | using FluentAssertions;
2 | using TechBuddy.Extensions.OpenApi;
3 | using NUnit.Framework;
4 |
5 | namespace SwaggerExtension.Tests;
6 | public sealed class SwaggerConfigTests
7 | {
8 |
9 | #region SubConfigs Null Check Tests
10 |
11 | [Test]
12 | public void WhenCreated_BeararTokenConfig_ShouldBeNull()
13 | {
14 | // Arrange
15 | var config = new SwaggerConfig();
16 |
17 | // Assert
18 |
19 | config.BearerConfig.Should().BeNull();
20 | }
21 |
22 | [Test]
23 | public void WhenCreated_SwaggerDocConfig_ShouldBeNull()
24 | {
25 | // Arrange
26 | var config = new SwaggerConfig();
27 |
28 |
29 | // Assert
30 | config.XmlDocConfig.Should().BeNull();
31 | }
32 |
33 | [Test]
34 | public void WhenCreated_ResponseTypeModelProviderConfig_ShouldBeNull()
35 | {
36 | // Arrange
37 | var config = new SwaggerConfig();
38 |
39 |
40 | // Assert
41 | config.ResponseTypeModelProviderConfig.Should().BeNull();
42 | }
43 |
44 | #endregion
45 |
46 | #region AddHeader Tests
47 |
48 | [Test]
49 | public void AddHeader_ShouldAddToHeaders()
50 | {
51 | // Arrange
52 | var config = new SwaggerConfig();
53 | var key = "key";
54 | var value = "value";
55 |
56 | // Action
57 | config.AddHeader(key, value);
58 |
59 | // Assert
60 | config.Headers.Should().ContainKey(key);
61 | config.Headers.Should().ContainValue(value);
62 | }
63 |
64 | [Test]
65 | public void AddHeader_WhenCalledTwice_ShouldReturnArgumentException()
66 | {
67 | // Arrange
68 | var config = new SwaggerConfig();
69 | var key = "key";
70 | var value = "value";
71 |
72 | // Action
73 | config.AddHeader(key, value);
74 |
75 | Action secondAddHeaderAction = () => config.AddHeader(key, value);
76 |
77 | // Assert
78 | secondAddHeaderAction.Should().Throw();
79 | }
80 |
81 | [Test]
82 | public void AddHeader_WithNullKey_ShouldReturnArgumentNullException()
83 | {
84 | // Arrange
85 | var config = new SwaggerConfig();
86 | string key = null;
87 | var value = "value";
88 |
89 | // Action
90 | Action secondAddHeaderAction = () => config.AddHeader(key, value);
91 |
92 | // Assert
93 | secondAddHeaderAction.Should().Throw();
94 | }
95 |
96 | [Test]
97 | public void AddHeader_WithEmptyKey_ShouldReturnArgumentNullException()
98 | {
99 | // Arrange
100 | var config = new SwaggerConfig();
101 | var key = "";
102 | var value = "value";
103 |
104 | // Action
105 | Action secondAddHeaderAction = () => config.AddHeader(key, value);
106 |
107 | // Assert
108 | secondAddHeaderAction.Should().Throw();
109 | }
110 |
111 |
112 |
113 | [Test]
114 | public void AddHeaderWithAction_ShouldAddToHeaders()
115 | {
116 | // Arrange
117 | var config = new SwaggerConfig();
118 | var key = "key";
119 | var value = "value";
120 |
121 | // Action
122 | config.AddHeader(key, () => value);
123 |
124 | // Assert
125 | config.Headers.Should().ContainKey(key);
126 | config.Headers.Should().ContainValue(value);
127 | }
128 |
129 | [Test]
130 | public void AddHeaderWithAction_WhenCalledTwice_ShouldReturnArgumentException()
131 | {
132 | // Arrange
133 | var config = new SwaggerConfig();
134 | var key = "key";
135 | var value = "value";
136 |
137 | // Action
138 | config.AddHeader(key, value);
139 |
140 | Action secondAddHeaderAction = () => config.AddHeader(key, value);
141 |
142 | // Assert
143 | secondAddHeaderAction.Should().Throw();
144 | }
145 |
146 | [Test]
147 | public void AddHeaderWithAction_WithNullKey_ShouldReturnArgumentNullException()
148 | {
149 | // Arrange
150 | var config = new SwaggerConfig();
151 | string key = null;
152 | var value = "value";
153 |
154 | // Action
155 | Action secondAddHeaderAction = () => config.AddHeader(key, value);
156 |
157 | // Assert
158 | secondAddHeaderAction.Should().Throw();
159 | }
160 |
161 | [Test]
162 | public void AddHeaderWithAction_WithEmptyKey_ShouldReturnArgumentNullException()
163 | {
164 | // Arrange
165 | var config = new SwaggerConfig();
166 | var key = "";
167 |
168 | // Action
169 | Action secondAddHeaderAction = () => config.AddHeader(key, () => "");
170 |
171 | // Assert
172 | secondAddHeaderAction.Should().Throw();
173 | }
174 |
175 | #endregion
176 | }
177 |
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension.Tests/SwaggerDocConfigTests.cs:
--------------------------------------------------------------------------------
1 | using FluentAssertions;
2 | using TechBuddy.Extensions.OpenApi;
3 | using NUnit.Framework;
4 |
5 | namespace SwaggerExtension.Tests;
6 | public sealed class SwaggerDocConfigTests
7 | {
8 | [Test]
9 | public void XmlDocEnabled_WhenSetFilePath_ShouldBeEnabled()
10 | {
11 | // Arrange
12 | var config = new SwaggerDocConfig
13 | {
14 | XmlDocEnabled = false,
15 | // Action
16 | XmlFilePath = "C:\\test.xml"
17 | };
18 |
19 | // Assert
20 | config.XmlDocEnabled.Should().BeTrue();
21 | }
22 |
23 |
24 | [Test]
25 | public void XmlFilePath_WhenSetFilePath_ShouldBeSame()
26 | {
27 | // Arrange
28 | var filePath = "C:\\test.xml";
29 |
30 |
31 | // Action
32 | var config = new SwaggerDocConfig
33 | {
34 | XmlDocEnabled = false,
35 | XmlFilePath = filePath
36 | };
37 |
38 | // Assert
39 | config.XmlFilePath.Should().Be(filePath);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension.Tests/SwaggerExtension.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 |
8 | false
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension.Tests/Usings.cs:
--------------------------------------------------------------------------------
1 | global using NUnit.Framework;
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension/Extensions/SwaggerDependencyInjectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.AspNetCore.Mvc.ApiExplorer;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using Microsoft.OpenApi.Models;
5 | using Swashbuckle.AspNetCore.SwaggerUI;
6 | using TechBuddy.Extensions.OpenApi.Infrastructure;
7 | using TechBuddy.Extensions.OpenApi.Infrastructure.OperationFilters;
8 | using TechBuddy.Extensions.OpenApi.Infrastructure.ProduceResponseType;
9 |
10 | namespace TechBuddy.Extensions.OpenApi;
11 |
12 | ///
13 | /// The extension class for OpenApi
14 | ///
15 | public static class SwaggerDependencyInjectionExtensions
16 | {
17 |
18 | ///
19 | /// This is used to configure the Swagger implementation with default configuration
20 | ///
21 | /// IServiceCollection
22 | /// returns services
23 | public static IServiceCollection ConfigureTechBuddySwagger(this IServiceCollection services)
24 | {
25 | return ConfigureTechBuddySwagger(services, opt =>
26 | {
27 | // Default configs
28 | });
29 | }
30 |
31 | ///
32 | /// This is used to configure the Swagger implementation
33 | ///
34 | /// IServiceCollection
35 | /// The configuration to customize the Swagger
36 | /// returns services
37 | public static IServiceCollection ConfigureTechBuddySwagger(this IServiceCollection services, Action configAction)
38 | {
39 | SwaggerConfig config = new();
40 | configAction(config);
41 | services.AddSingleton(config);
42 |
43 | if (config.ResponseTypeModelProviderConfig is not null)
44 | {
45 | services.ConfigureTechBuddyResponesTypemodelProvider(config.ResponseTypeModelProviderConfig);
46 | }
47 |
48 | services.AddSwaggerGen(c =>
49 | {
50 | if (config.EnabledJsonIgnoreFilter)
51 | {
52 | c.OperationFilter();
53 | }
54 |
55 | c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
56 |
57 | if (config.BearerConfig is not null)
58 | {
59 | c.AddSecurityDefinition(config.BearerConfig.HeaderKey, new OpenApiSecurityScheme()
60 | {
61 | In = ParameterLocation.Header,
62 | BearerFormat = SwaggerConstants.BearerFormat,
63 | Type = SecuritySchemeType.Http,
64 | Scheme = config.BearerConfig.HeaderKey,
65 | Description = config.BearerConfig.BearerDescription
66 | });
67 |
68 | c.OperationFilter(config.BearerConfig);
69 | }
70 |
71 | if (config.Headers is not null)
72 | {
73 | foreach (var header in config.Headers)
74 | {
75 | c.OperationFilter(header.Key, header.Value);
76 | }
77 | }
78 | });
79 |
80 | services.ConfigureOptions();
81 |
82 | return services;
83 | }
84 |
85 | ///
86 | /// This is used to enable Swagger in your WebApi
87 | ///
88 | /// IApplicationBuilder
89 | /// This is used to enable Swagger with ApiVersioning Features
90 | /// returns app
91 | public static IApplicationBuilder UseTechBuddySwagger(this IApplicationBuilder app, bool withApiVersioning = true)
92 | {
93 | IApiVersionDescriptionProvider apiVersioningProvider = withApiVersioning ? app.ApplicationServices.GetService() : null;
94 | SwaggerConfig swaggerConfig = app.ApplicationServices.GetRequiredService();
95 |
96 | app.UseSwagger();
97 |
98 | app.UseSwaggerUI(options =>
99 | {
100 | SetOptionDetails(options);
101 |
102 | SetSwaggerEndPoint(options, swaggerConfig, apiVersioningProvider);
103 | });
104 |
105 | return app;
106 | }
107 |
108 | private static void SetOptionDetails(SwaggerUIOptions options)
109 | {
110 | options.DefaultModelExpandDepth(2);
111 | options.DefaultModelRendering(ModelRendering.Model);
112 | options.DefaultModelsExpandDepth(-1);
113 | options.DisplayOperationId();
114 | options.DisplayRequestDuration();
115 | options.DocExpansion(DocExpansion.List);
116 | options.EnableDeepLinking();
117 | options.EnableFilter();
118 | options.MaxDisplayedTags(5);
119 | options.ShowExtensions();
120 | options.ShowCommonExtensions();
121 | options.EnableValidator();
122 | }
123 |
124 | private static void SetSwaggerEndPoint(SwaggerUIOptions options, SwaggerConfig config, IApiVersionDescriptionProvider provider = null)
125 | {
126 | if (provider is null)
127 | {
128 | string endpointName = !string.IsNullOrWhiteSpace(config?.ProjectName)
129 | ? config.ProjectName
130 | : SwaggerConstants.DefaultSwaggerApiVersion;
131 |
132 | options.SwaggerEndpoint(SwaggerConstants.DefaultSwaggerEndpoint, $"{endpointName}");
133 | return;
134 | }
135 |
136 | foreach (var description in provider.ApiVersionDescriptions)
137 | {
138 | var swaggerFileUrl = $"/{SwaggerConstants.DefaultSwaggerEndpointFileDirectory}/{description.GroupName}/{SwaggerConstants.DefaultSwaggerEndpointFileName}";
139 | var name = $"{config?.ProjectName} {description.GroupName.ToUpperInvariant()}";
140 | options.SwaggerEndpoint(swaggerFileUrl, name);
141 | }
142 | }
143 | }
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension/Infrastructure/ConfigModels/DefaultHttpStatusCodeForHttpMethod.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 |
3 | namespace TechBuddy.Extensions.OpenApi;
4 |
5 | ///
6 | /// The DefaultHttpStatusCodeForHttpMethod to save HttpStatusCode for HttpMethod
7 | ///
8 | public class DefaultHttpStatusCodeForHttpMethod
9 | {
10 | ///
11 | /// The consturctor to create this class
12 | ///
13 | /// The http method
14 | /// The http status code
15 | public DefaultHttpStatusCodeForHttpMethod(HttpMethod httpMethod, HttpStatusCode httpStatusCode)
16 | {
17 | HttpMethod = httpMethod;
18 | HttpStatusCode = httpStatusCode;
19 | }
20 |
21 | ///
22 | /// The http method you want to add the status code for
23 | ///
24 | public HttpMethod HttpMethod { get; set; }
25 |
26 | ///
27 | /// The http status code
28 | ///
29 | public HttpStatusCode HttpStatusCode { get; set; }
30 | }
31 |
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension/Infrastructure/ConfigModels/SwaggerBearerConfig.cs:
--------------------------------------------------------------------------------
1 | using TechBuddy.Extensions.OpenApi.Infrastructure;
2 |
3 | namespace TechBuddy.Extensions.OpenApi;
4 |
5 | ///
6 | /// The config to set the Auth in Swagger implementation
7 | ///
8 | public class SwaggerBearerConfig
9 | {
10 | ///
11 | /// true when JWT/Auth is enabled on Swagger UI
12 | ///
13 | public bool AuthEnabled { get; set; }
14 |
15 | ///
16 | /// The bearer key to use as a prefix with JWT in header
17 | ///
18 | public string HeaderKey { get; set; } = SwaggerConstants.BearerHeaderKey;
19 |
20 | ///
21 | /// The description will be shown on Swagger UI Auth
22 | ///
23 | public string BearerDescription => SwaggerConstants.DefaultBearerDescription;
24 | }
25 |
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension/Infrastructure/ConfigModels/SwaggerConfig.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using TechBuddy.Extensions.OpenApi.Infrastructure;
3 |
4 | namespace TechBuddy.Extensions.OpenApi;
5 |
6 | ///
7 | /// The config to customize Swagger implementation
8 | ///
9 | public class SwaggerConfig
10 | {
11 | ///
12 | /// The list of Header
13 | ///
14 | public Dictionary Headers { get; private set; } = new Dictionary();
15 |
16 |
17 | ///
18 | /// The project name which will be used in Swagger UI and also documentation
19 | ///
20 | public string ProjectName { get; set; } = Assembly.GetCallingAssembly().GetName()?.Name ?? SwaggerConstants.DefaultProjectName;
21 |
22 | ///
23 | /// The Auth config if Bearer/JWT is enabled
24 | ///
25 | public SwaggerBearerConfig BearerConfig { get; set; }
26 |
27 | ///
28 | /// Xml document config to customize Swagger UI to show the doc comments
29 | ///
30 | public SwaggerDocConfig XmlDocConfig { get; set; }
31 |
32 | ///
33 | /// The config for ResponseTypeModelProvider
34 | ///
35 | public ResponseTypeModelProviderConfig ResponseTypeModelProviderConfig { get; set; }
36 |
37 | ///
38 | /// true when JsonIgnore is enabled on Swagger UI
39 | ///
40 | public bool EnabledJsonIgnoreFilter { get; set; } = true;
41 |
42 |
43 |
44 |
45 | ///
46 | /// Enables to add header value when requesting via Swagger UI
47 | ///
48 | /// The header key
49 | /// The header value
50 | /// return itself
51 | /// This is thrown when duplicate key is added
52 | /// This is thrown when key is null or empty
53 | public SwaggerConfig AddHeader(string key, string value)
54 | {
55 | if (string.IsNullOrEmpty(key))
56 | throw new ArgumentNullException(key);
57 |
58 | var added = Headers.TryAdd(key, value);
59 |
60 | return added
61 | ? this
62 | : throw new ArgumentException($"{key} already exists in header");
63 | }
64 |
65 | ///
66 | /// Enables to add header value when requesting via Swagger UI
67 | ///
68 | /// The header key
69 | /// The action to get the header value
70 | /// return itself
71 | /// This is thrown when duplicate key is added
72 | /// This is thrown when key is null or empty
73 | public SwaggerConfig AddHeader(string key, Func valueFunc)
74 | {
75 | var headerValue = valueFunc();
76 | return AddHeader(key, headerValue);
77 | }
78 |
79 |
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension/Infrastructure/ConfigModels/SwaggerDocConfig.cs:
--------------------------------------------------------------------------------
1 | namespace TechBuddy.Extensions.OpenApi;
2 |
3 | ///
4 | /// The Xml Documentation Config for Swagger
5 | ///
6 | public class SwaggerDocConfig
7 | {
8 | private string xmlFilePath;
9 |
10 | ///
11 | /// If xml documentation is enabled
12 | ///
13 | public bool XmlDocEnabled { get; set; }
14 |
15 | ///
16 | /// The xml file path
17 | ///
18 | public string XmlFilePath
19 | {
20 | get => xmlFilePath;
21 | set
22 | {
23 | xmlFilePath = value;
24 | if (!string.IsNullOrEmpty(value))
25 | XmlDocEnabled = true;
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension/Infrastructure/Configurations/ConfigureSwaggerOptions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc.ApiExplorer;
2 | using Microsoft.Extensions.DependencyInjection;
3 | using Microsoft.Extensions.Options;
4 | using Microsoft.OpenApi.Models;
5 | using Swashbuckle.AspNetCore.SwaggerGen;
6 |
7 | namespace TechBuddy.Extensions.OpenApi;
8 |
9 | internal class ConfigureSwaggerOptions : IConfigureNamedOptions
10 | {
11 | private readonly IApiVersionDescriptionProvider provider;
12 | private readonly SwaggerConfig config;
13 |
14 | ///
15 | /// The constructure with so DI can create by providing in case of ApiVersioning is ENABLED
16 | ///
17 | ///
18 | ///
19 | public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider, SwaggerConfig config)
20 | {
21 | this.provider = provider;
22 | this.config = config;
23 | }
24 |
25 | ///
26 | /// The parameterless constructure so DI can create without providing in case of ApiVersioning is DISABLED
27 | ///
28 | public ConfigureSwaggerOptions(SwaggerConfig config)
29 | {
30 | this.config = config;
31 | }
32 |
33 | public void Configure(string name, SwaggerGenOptions options)
34 | {
35 | Configure(options);
36 | }
37 |
38 | public void Configure(SwaggerGenOptions options)
39 | {
40 | if (provider?.ApiVersionDescriptions is not null)
41 | {
42 | foreach (var description in provider.ApiVersionDescriptions)
43 | {
44 | options.SwaggerDoc(description.GroupName, CreateVersionInfo(description));
45 | }
46 | }
47 |
48 | if (config.XmlDocConfig is null || !config.XmlDocConfig.XmlDocEnabled)
49 | return;
50 |
51 | var fileExt = Path.GetExtension(config.XmlDocConfig.XmlFilePath);
52 | if (string.IsNullOrWhiteSpace(fileExt) || !fileExt.Equals(".xml", StringComparison.OrdinalIgnoreCase))
53 | throw new ArgumentException("XMLDocFile extension must be .xml!");
54 |
55 | if (!File.Exists(config.XmlDocConfig.XmlFilePath))
56 | throw new Exception($"XMLDocFile could not found in {config.XmlDocConfig.XmlFilePath}");
57 |
58 | options.IncludeXmlComments(config.XmlDocConfig.XmlFilePath, true);
59 | }
60 |
61 | private OpenApiInfo CreateVersionInfo(ApiVersionDescription description)
62 | {
63 | var info = new OpenApiInfo()
64 | {
65 | Title = config.ProjectName,
66 | Version = description.ApiVersion.ToString(),
67 | Contact = new OpenApiContact()
68 | {
69 | Name = "https://youtube.com/c/TechBuddyTR"
70 | }
71 | };
72 |
73 | if (description.IsDeprecated)
74 | {
75 | info.Description += " This API has been deprecated.";
76 | }
77 |
78 | return info;
79 | }
80 | }
81 |
82 |
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension/Infrastructure/OperationFilters/AddRequiredHeaderParameter.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.OpenApi.Any;
2 | using Microsoft.OpenApi.Models;
3 | using Swashbuckle.AspNetCore.SwaggerGen;
4 |
5 | namespace TechBuddy.Extensions.OpenApi.Infrastructure.OperationFilters;
6 | internal class AddRequiredHeaderParameter : IOperationFilter
7 | {
8 | private readonly string headerKey;
9 | private readonly string headerValue;
10 |
11 | public AddRequiredHeaderParameter(string headerKey, string headerValue)
12 | {
13 | this.headerKey = headerKey;
14 | this.headerValue = headerValue;
15 | }
16 |
17 | public void Apply(OpenApiOperation operation, OperationFilterContext context)
18 | {
19 | operation.Parameters ??= new List();
20 |
21 | operation.Parameters.Add(new OpenApiParameter()
22 | {
23 | Name = headerKey,
24 | In = ParameterLocation.Header,
25 | Required = false,
26 | Schema = new OpenApiSchema()
27 | {
28 | Type = "string",
29 | Default = new OpenApiString(headerValue)
30 | }
31 | });
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension/Infrastructure/OperationFilters/AuthenticationRequirements.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.OpenApi.Models;
2 | using Swashbuckle.AspNetCore.SwaggerGen;
3 |
4 | namespace TechBuddy.Extensions.OpenApi.Infrastructure.OperationFilters;
5 |
6 | internal class AuthenticationRequirements : IOperationFilter
7 | {
8 | private readonly SwaggerBearerConfig config;
9 |
10 | public AuthenticationRequirements(SwaggerBearerConfig config)
11 | {
12 | this.config = config;
13 | }
14 |
15 | public void Apply(OpenApiOperation operation, OperationFilterContext context)
16 | {
17 | operation.Security ??= new List();
18 |
19 | var scheme = new OpenApiSecurityScheme
20 | {
21 | Reference = new OpenApiReference
22 | {
23 | Type = ReferenceType.SecurityScheme,
24 | Id = config.HeaderKey
25 | }
26 | };
27 |
28 | var req = new OpenApiSecurityRequirement
29 | {
30 | { scheme, new List() }
31 | };
32 |
33 | operation.Security.Add(req);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension/Infrastructure/OperationFilters/JsonIgnoreOperationFilter.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc.ApiExplorer;
2 | using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
3 | using Microsoft.OpenApi.Models;
4 | using Swashbuckle.AspNetCore.SwaggerGen;
5 | using System.Text.Json.Serialization;
6 |
7 | namespace TechBuddy.Extensions.OpenApi.Infrastructure.OperationFilters
8 | {
9 | internal class JsonIgnoreOperationFilter : IOperationFilter
10 | {
11 | public void Apply(OpenApiOperation operation, OperationFilterContext context)
12 | {
13 | IList parameterDescriptions = context.ApiDescription.ParameterDescriptions;
14 |
15 | foreach (ApiParameterDescription parameterDescription in parameterDescriptions)
16 | {
17 | bool? ignore = parameterDescription.ModelMetadata is DefaultModelMetadata metaData
18 | ? metaData.Attributes.PropertyAttributes?.Any(x => x is JsonIgnoreAttribute)
19 | : false;
20 |
21 | if (ignore.HasValue && ignore.Value)
22 | {
23 | OpenApiParameter apiParameter = operation.Parameters.FirstOrDefault(x => x.Name == parameterDescription.Name);
24 |
25 | if (apiParameter is not null)
26 | {
27 | operation.Parameters.Remove(apiParameter);
28 | }
29 | }
30 | }
31 |
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension/Infrastructure/ProduceResponseType/ProduceResponseTypeModelProvider.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Microsoft.AspNetCore.Mvc.ApplicationModels;
3 | using Microsoft.AspNetCore.Mvc.Routing;
4 |
5 | namespace TechBuddy.Extensions.OpenApi.Infrastructure.ProduceResponseType;
6 | internal class ProduceResponseTypeModelProvider : IApplicationModelProvider
7 | {
8 | private readonly ResponseTypeModelProviderConfig config;
9 |
10 | public ProduceResponseTypeModelProvider(ResponseTypeModelProviderConfig config)
11 | {
12 | this.config = config;
13 | }
14 |
15 | public int Order => 1;
16 |
17 | public void OnProvidersExecuted(ApplicationModelProviderContext context) { }
18 |
19 | // This functions tends to add default ResponseType attribute for the actions that doesn't have
20 | // 200 => OK and 400 => BadRequest (in case of validation error)
21 | public void OnProvidersExecuting(ApplicationModelProviderContext context)
22 | {
23 | var controllers = GetTrimmedControllers(context);
24 |
25 | foreach (ControllerModel controller in controllers)
26 | {
27 | IEnumerable<(ActionModel actionModel, IEnumerable httpMethods)> actions
28 | = GetTrimmedActions(controller);
29 |
30 | foreach ((ActionModel actionModel, IEnumerable httpMethods) actionsWithMethods in actions)
31 | {
32 | AddFiltersToAction(actionsWithMethods.actionModel, actionsWithMethods.httpMethods);
33 | }
34 | }
35 | }
36 |
37 |
38 |
39 |
40 | #region Private Methods
41 |
42 |
43 | private void AddFiltersToAction(ActionModel action, IEnumerable httpMethods)
44 | {
45 | // Add Default Status Code
46 | foreach (var defaultStatusCode in config.DefaultStatusCodes)
47 | {
48 | var eligible = httpMethods.Any(i => i == defaultStatusCode.HttpMethod.ToString());
49 | if (!eligible)
50 | continue;
51 |
52 | Type returnType = GetActionReturnModelType(action);
53 |
54 | // If DefaultType
55 | if (config.DefaultTypes.Contains(returnType))
56 | {
57 | var typeBaseStatusCodeFound = config.TypeBaseStatusCodes.Any(i => i.Key == defaultStatusCode.HttpStatusCode);
58 |
59 | if (typeBaseStatusCodeFound)
60 | returnType = config.TypeBaseStatusCodes[defaultStatusCode.HttpStatusCode];
61 | }
62 |
63 | AddFilterToActionModel(action,
64 | new ProducesResponseTypeAttribute(returnType, (int)defaultStatusCode.HttpStatusCode));
65 | }
66 |
67 |
68 | }
69 |
70 | ///
71 | /// Exclude the provided controllers. And also trims "controller" key in the name of the Controllers
72 | ///
73 | /// The ApplicationModelProviderContext param provided by .NET
74 | /// The list of Controllers
75 | private IEnumerable GetTrimmedControllers(ApplicationModelProviderContext context)
76 | {
77 | const string controllerKey = "controller";
78 | var result = new List();
79 |
80 | var trimmedExcludedList = config.ExcludedControllers.Select(i => i.Replace(controllerKey, "", StringComparison.OrdinalIgnoreCase));
81 |
82 | foreach (var controller in context.Result.Controllers)
83 | {
84 | var controllerName = controller.ControllerName.Replace(controllerKey, "", StringComparison.OrdinalIgnoreCase);
85 |
86 | var exclude = trimmedExcludedList.Contains(controllerName);
87 |
88 | if (!exclude)
89 | result.Add(controller);
90 | }
91 |
92 | return result;
93 | }
94 |
95 | private IEnumerable<(ActionModel actionModel, IEnumerable httpMethods)> GetTrimmedActions(ControllerModel controller)
96 | {
97 | List<(ActionModel actionModel, IEnumerable httpMethods)> result = new();
98 |
99 | foreach (ActionModel action in controller.Actions)
100 | {
101 | var notEligible = IfExcludedAction(action);
102 | if (notEligible)
103 | continue;
104 |
105 | var httpMethods = action.Attributes
106 | .OfType()
107 | .SelectMany(x => x.HttpMethods).Distinct();
108 |
109 | notEligible = config.DefaultStatusCodes.Any(i => httpMethods.Contains(i.ToString()));
110 |
111 | if (notEligible)
112 | continue;
113 |
114 | result.Add((action, httpMethods));
115 | }
116 |
117 | return result;
118 | }
119 |
120 | private bool IfExcludedAction(ActionModel actionModel)
121 | {
122 | // Generate possible action names to check if excluded
123 | var possibleActionNames = new[]
124 | {
125 | actionModel.ActionName,
126 | $"{actionModel.Controller.ControllerName}.{actionModel.ActionName}",
127 | $"{actionModel.Controller.ControllerName}Controller.{actionModel.ActionName}",
128 | };
129 |
130 | var excluded = config.ExcludedActions.Any(i => possibleActionNames.Contains(i));
131 |
132 | return excluded;
133 | }
134 |
135 | private Type GetActionReturnModelType(ActionModel actionModel)
136 | {
137 | Type returnType = actionModel.ActionMethod.ReturnType;
138 |
139 | while (returnType.GetGenericArguments()?.Length > 0)
140 | {
141 | returnType = returnType.GetGenericArguments().First();
142 | }
143 |
144 | return returnType;
145 | }
146 |
147 | private static void AddFilterToActionModel(ActionModel action,
148 | ProducesResponseTypeAttribute filter)
149 | {
150 | bool alreadyAdded = action.Filters
151 | .Where(i => i is ProducesResponseTypeAttribute)
152 | .Select(i => (ProducesResponseTypeAttribute)i)
153 | .Any(i => i.StatusCode == filter.StatusCode);
154 |
155 | if (alreadyAdded) return;
156 |
157 | action.Filters.Add(filter);
158 |
159 | // To say it produces Json
160 | action.Filters.Add(new ProducesAttribute(SwaggerConstants.DefaultResponseContentType));
161 | }
162 |
163 | #endregion
164 | }
165 |
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension/Infrastructure/ProduceResponseType/ResponseTypeModelProviderDependencyInjection.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc.ApplicationModels;
2 | using Microsoft.Extensions.DependencyInjection;
3 |
4 | namespace TechBuddy.Extensions.OpenApi.Infrastructure.ProduceResponseType;
5 | ///
6 | /// The extension class for ResponseTypeModelProviderDependencyInjection
7 | ///
8 | internal static class ResponseTypeModelProviderDependencyInjection
9 | {
10 | internal static IServiceCollection ConfigureTechBuddyResponesTypemodelProvider(this IServiceCollection services,
11 | ResponseTypeModelProviderConfig config)
12 | {
13 | services.AddSingleton(config);
14 | services.AddSingleton();
15 |
16 | return services;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension/Infrastructure/SwaggerConstants.cs:
--------------------------------------------------------------------------------
1 | namespace TechBuddy.Extensions.OpenApi.Infrastructure;
2 | internal class SwaggerConstants
3 | {
4 | internal const string DefaultProjectName = "TechBuddy.Extensions.ProjectName";
5 |
6 | internal const string DefaultSwaggerEndpointFileName = "swagger.json";
7 | internal const string DefaultSwaggerEndpointFileDirectory = "swagger";
8 | internal const string DefaultSwaggerEndpoint = $"/{DefaultSwaggerEndpointFileDirectory}/v1/{DefaultSwaggerEndpointFileName}";
9 | internal const string DefaultSwaggerApiVersion = "1";
10 |
11 | internal const string DefaultResponseContentType = "application/json";
12 |
13 | internal const string BearerFormat = "JWT";
14 | internal const string BearerHeaderKey = "bearer";
15 | internal static string DefaultBearerDescription
16 | => "JWT Authorization header using the {HeaderKey} scheme. \r\n\r\n Enter '{HeaderKey}' [space] and then your token in the text input below.\r\n\r\nExample: \"{HeaderKey} {TOKEN}\"".Replace("{HeaderKey}", BearerHeaderKey);
17 | }
18 |
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension/RoadMap.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 | - Swagger Headers
4 | - Authorization - Bearer
5 | - HeaderKey
6 | - Description
7 | - XmlDoc Support
8 | - FilePath
9 | - ApiVersioning - Swagger UI Implementation
10 |
11 |
12 | - ResponseTypeModelProvider
13 | - Default HttpStatusCodes
14 | - Default Types (Task, Task, Task)
15 | - Default HttpMethods
16 | - Exclude Controller
17 | - Exclude Action
18 | - Default StatusCode for HttpMethods
19 | - Specific Models for Specific StatusCodes
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension/SwaggerExtension.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 |
7 |
8 | TechBuddy.OpenApi
9 | TechBuddy.Extensions.OpenApi
10 |
11 |
12 |
13 | true
14 | true
15 | true
16 | TechBuddy.Extensions.OpenApi
17 | OpenApi; OpenApiExtension; Swagger; SwaggerExtension;
18 |
19 |
20 |
21 | TechBuddyTR Youtube
22 | Salih Cantekin, Community
23 | Provides extension methods for OpenApi Clients such as Swagger
24 | logo.png
25 | TechBuddyTR Youtube
26 | LICENSE.txt
27 | README.md
28 | https://github.com/TechBuddyTR/TechBuddy.Extensions
29 | https://github.com/TechBuddyTR/TechBuddy.Extensions
30 | git
31 |
32 |
33 |
34 |
35 | Always
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension/licenses/license.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 [Salih Cantekin]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/SwaggerExtension/SwaggerExtension/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TechBuddyTR/TechBuddy.Extensions/df08c47466a9a5cf5d5bd472865743f2a6c2b8ae/src/SwaggerExtension/SwaggerExtension/logo.png
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests.AF/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # Azure Functions localsettings file
5 | local.settings.json
6 |
7 | # User-specific files
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Build results
17 | [Dd]ebug/
18 | [Dd]ebugPublic/
19 | [Rr]elease/
20 | [Rr]eleases/
21 | x64/
22 | x86/
23 | bld/
24 | [Bb]in/
25 | [Oo]bj/
26 | [Ll]og/
27 |
28 | # Visual Studio 2015 cache/options directory
29 | .vs/
30 | # Uncomment if you have tasks that create the project's static files in wwwroot
31 | #wwwroot/
32 |
33 | # MSTest test Results
34 | [Tt]est[Rr]esult*/
35 | [Bb]uild[Ll]og.*
36 |
37 | # NUNIT
38 | *.VisualState.xml
39 | TestResult.xml
40 |
41 | # Build Results of an ATL Project
42 | [Dd]ebugPS/
43 | [Rr]eleasePS/
44 | dlldata.c
45 |
46 | # DNX
47 | project.lock.json
48 | project.fragment.lock.json
49 | artifacts/
50 |
51 | *_i.c
52 | *_p.c
53 | *_i.h
54 | *.ilk
55 | *.meta
56 | *.obj
57 | *.pch
58 | *.pdb
59 | *.pgc
60 | *.pgd
61 | *.rsp
62 | *.sbr
63 | *.tlb
64 | *.tli
65 | *.tlh
66 | *.tmp
67 | *.tmp_proj
68 | *.log
69 | *.vspscc
70 | *.vssscc
71 | .builds
72 | *.pidb
73 | *.svclog
74 | *.scc
75 |
76 | # Chutzpah Test files
77 | _Chutzpah*
78 |
79 | # Visual C++ cache files
80 | ipch/
81 | *.aps
82 | *.ncb
83 | *.opendb
84 | *.opensdf
85 | *.sdf
86 | *.cachefile
87 | *.VC.db
88 | *.VC.VC.opendb
89 |
90 | # Visual Studio profiler
91 | *.psess
92 | *.vsp
93 | *.vspx
94 | *.sap
95 |
96 | # TFS 2012 Local Workspace
97 | $tf/
98 |
99 | # Guidance Automation Toolkit
100 | *.gpState
101 |
102 | # ReSharper is a .NET coding add-in
103 | _ReSharper*/
104 | *.[Rr]e[Ss]harper
105 | *.DotSettings.user
106 |
107 | # JustCode is a .NET coding add-in
108 | .JustCode
109 |
110 | # TeamCity is a build add-in
111 | _TeamCity*
112 |
113 | # DotCover is a Code Coverage Tool
114 | *.dotCover
115 |
116 | # NCrunch
117 | _NCrunch_*
118 | .*crunch*.local.xml
119 | nCrunchTemp_*
120 |
121 | # MightyMoose
122 | *.mm.*
123 | AutoTest.Net/
124 |
125 | # Web workbench (sass)
126 | .sass-cache/
127 |
128 | # Installshield output folder
129 | [Ee]xpress/
130 |
131 | # DocProject is a documentation generator add-in
132 | DocProject/buildhelp/
133 | DocProject/Help/*.HxT
134 | DocProject/Help/*.HxC
135 | DocProject/Help/*.hhc
136 | DocProject/Help/*.hhk
137 | DocProject/Help/*.hhp
138 | DocProject/Help/Html2
139 | DocProject/Help/html
140 |
141 | # Click-Once directory
142 | publish/
143 |
144 | # Publish Web Output
145 | *.[Pp]ublish.xml
146 | *.azurePubxml
147 | # TODO: Comment the next line if you want to checkin your web deploy settings
148 | # but database connection strings (with potential passwords) will be unencrypted
149 | #*.pubxml
150 | *.publishproj
151 |
152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
153 | # checkin your Azure Web App publish settings, but sensitive information contained
154 | # in these scripts will be unencrypted
155 | PublishScripts/
156 |
157 | # NuGet Packages
158 | *.nupkg
159 | # The packages folder can be ignored because of Package Restore
160 | **/packages/*
161 | # except build/, which is used as an MSBuild target.
162 | !**/packages/build/
163 | # Uncomment if necessary however generally it will be regenerated when needed
164 | #!**/packages/repositories.config
165 | # NuGet v3's project.json files produces more ignoreable files
166 | *.nuget.props
167 | *.nuget.targets
168 |
169 | # Microsoft Azure Build Output
170 | csx/
171 | *.build.csdef
172 |
173 | # Microsoft Azure Emulator
174 | ecf/
175 | rcf/
176 |
177 | # Windows Store app package directories and files
178 | AppPackages/
179 | BundleArtifacts/
180 | Package.StoreAssociation.xml
181 | _pkginfo.txt
182 |
183 | # Visual Studio cache files
184 | # files ending in .cache can be ignored
185 | *.[Cc]ache
186 | # but keep track of directories ending in .cache
187 | !*.[Cc]ache/
188 |
189 | # Others
190 | ClientBin/
191 | ~$*
192 | *~
193 | *.dbmdl
194 | *.dbproj.schemaview
195 | *.jfm
196 | *.pfx
197 | *.publishsettings
198 | node_modules/
199 | orleans.codegen.cs
200 |
201 | # Since there are multiple workflows, uncomment next line to ignore bower_components
202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
203 | #bower_components/
204 |
205 | # RIA/Silverlight projects
206 | Generated_Code/
207 |
208 | # Backup & report files from converting an old project file
209 | # to a newer Visual Studio version. Backup files are not needed,
210 | # because we have git ;-)
211 | _UpgradeReport_Files/
212 | Backup*/
213 | UpgradeLog*.XML
214 | UpgradeLog*.htm
215 |
216 | # SQL Server files
217 | *.mdf
218 | *.ldf
219 |
220 | # Business Intelligence projects
221 | *.rdl.data
222 | *.bim.layout
223 | *.bim_*.settings
224 |
225 | # Microsoft Fakes
226 | FakesAssemblies/
227 |
228 | # GhostDoc plugin setting file
229 | *.GhostDoc.xml
230 |
231 | # Node.js Tools for Visual Studio
232 | .ntvs_analysis.dat
233 |
234 | # Visual Studio 6 build log
235 | *.plg
236 |
237 | # Visual Studio 6 workspace options file
238 | *.opt
239 |
240 | # Visual Studio LightSwitch build output
241 | **/*.HTMLClient/GeneratedArtifacts
242 | **/*.DesktopClient/GeneratedArtifacts
243 | **/*.DesktopClient/ModelManifest.xml
244 | **/*.Server/GeneratedArtifacts
245 | **/*.Server/ModelManifest.xml
246 | _Pvt_Extensions
247 |
248 | # Paket dependency manager
249 | .paket/paket.exe
250 | paket-files/
251 |
252 | # FAKE - F# Make
253 | .fake/
254 |
255 | # JetBrains Rider
256 | .idea/
257 | *.sln.iml
258 |
259 | # CodeRush
260 | .cr/
261 |
262 | # Python Tools for Visual Studio (PTVS)
263 | __pycache__/
264 | *.pyc
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests.AF/Infrastructure/Helpers/Validators/TestValidator.cs:
--------------------------------------------------------------------------------
1 | using FluentValidation;
2 | using ValidationExtension.Tests.AF.Infrastructure.Models;
3 |
4 | namespace ValidationExtension.Tests.AF.Infrastructure.Helpers.Validators;
5 | public sealed class TestValidator : AbstractValidator
6 | {
7 | public TestValidator()
8 | {
9 | RuleFor(i => i.Id).GreaterThan(0).WithMessage("{PropertyName} cannot be zero!");
10 | RuleFor(i => i.Name).MinimumLength(3).WithMessage("{PropertyName} must be at least {MinLenght} character");
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests.AF/Infrastructure/Models/TestModel.cs:
--------------------------------------------------------------------------------
1 | namespace ValidationExtension.Tests.AF.Infrastructure.Models;
2 | public sealed class TestModel
3 | {
4 | public int Id { get; set; }
5 |
6 | public string Name { get; set; }
7 | }
8 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests.AF/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "ValidationExtension.Tests.AF": {
4 | "commandName": "Project",
5 | "commandLineArgs": "--port 7191",
6 | "launchBrowser": false
7 | }
8 | }
9 | }
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests.AF/Properties/serviceDependencies.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "appInsights1": {
4 | "type": "appInsights"
5 | },
6 | "storage1": {
7 | "type": "storage",
8 | "connectionId": "AzureWebJobsStorage"
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests.AF/Properties/serviceDependencies.local.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "appInsights1": {
4 | "type": "appInsights.sdk"
5 | },
6 | "storage1": {
7 | "type": "storage.emulator",
8 | "connectionId": "AzureWebJobsStorage"
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests.AF/Startup.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Azure.Functions.Extensions.DependencyInjection;
2 | using Microsoft.Extensions.Configuration;
3 | using System;
4 | using System.Diagnostics.CodeAnalysis;
5 | using TechBuddy.Extension.Validation.Extensions;
6 | using TransactionAccess.API;
7 |
8 | [assembly: FunctionsStartup(typeof(Startup))]
9 | namespace TransactionAccess.API;
10 |
11 | [ExcludeFromCodeCoverage]
12 | public class Startup : FunctionsStartup
13 | {
14 | ///
15 | /// Configures the specified builder.
16 | ///
17 | /// The builder.
18 | public override void Configure(IFunctionsHostBuilder builder)
19 | {
20 | var context = builder.GetContext();
21 |
22 | builder.Services.AddTechBuddyValidator();
23 | }
24 |
25 | ///
26 | /// Configures the application configuration.
27 | ///
28 | /// The builder.
29 | public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
30 | {
31 | builder.ConfigurationBuilder
32 | .SetBasePath(Environment.CurrentDirectory)
33 | #if DEBUG
34 | .AddJsonFile("local.settings.json", true, true)
35 | #endif
36 | .AddEnvironmentVariables();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests.AF/TestFunction.cs:
--------------------------------------------------------------------------------
1 | using FluentValidation;
2 | using Microsoft.AspNetCore.Http;
3 | using Microsoft.AspNetCore.Mvc;
4 | using Microsoft.Azure.WebJobs;
5 | using Microsoft.Azure.WebJobs.Extensions.Http;
6 | using System.Linq;
7 | using System.Threading.Tasks;
8 | using TechBuddy.Extension.Validation.Extensions;
9 | using ValidationExtension.Tests.AF.Infrastructure.Helpers.Validators;
10 | using ValidationExtension.Tests.AF.Infrastructure.Models;
11 |
12 | namespace ValidationExtension.Tests.AF;
13 |
14 | public class TestFunction
15 | {
16 | private readonly IValidator testValidator;
17 |
18 | public TestFunction(IValidator testValidator)
19 | {
20 | this.testValidator = testValidator;
21 | }
22 |
23 |
24 | [FunctionName("Function1")]
25 | public IActionResult Run(
26 | [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] TestModel testModel)
27 | {
28 | var validationResult = testValidator.Validate(testModel); // FluentValidation Validate
29 |
30 | if (!validationResult.IsValid)
31 | return new BadRequestObjectResult(validationResult.Errors.Select(i => i.ErrorMessage));
32 |
33 | return new OkObjectResult(testModel.Name);
34 | }
35 |
36 |
37 | [FunctionName("Function2")]
38 | public async Task RunTest(
39 | [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req)
40 | {
41 | var validationResult = await req.ValidateAsync(); // ValidationExtension Validate
42 |
43 | if (!validationResult.IsValid)
44 | return new BadRequestObjectResult(validationResult.Errors);
45 |
46 | var testModel = await req.ReadFromJsonAsync();
47 | //var testModel = validationResult.Model;
48 |
49 | return new OkObjectResult(testModel.Name);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests.AF/ValidationExtension.Tests.AF.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0
4 | v4
5 |
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | PreserveNewest
18 |
19 |
20 | PreserveNewest
21 | Never
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests.AF/host.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0",
3 | "logging": {
4 | "applicationInsights": {
5 | "samplingSettings": {
6 | "isEnabled": true,
7 | "excludedTypes": "Request"
8 | }
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests.WebApi/Controllers/TestController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using ValidationExtension.Tests.WebApi.Infrastructure.Models;
3 |
4 | namespace ValidationExtension.Tests.WebApi.Controllers;
5 |
6 | [ApiController]
7 | [Route("[controller]")]
8 | public class TestController : ControllerBase
9 | {
10 | //private readonly IValidator validator;
11 |
12 | //public TestController(IValidator validator)
13 | //{
14 | // this.validator = validator;
15 | //}
16 |
17 | [HttpPost]
18 |
19 | public ActionResult Post([FromBody] TestModel testModel)
20 | {
21 | //var result = validator.Validate(testModel);
22 | //if (!result.IsValid)
23 | // return BadRequest(result);
24 |
25 | return Ok(testModel.Name);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests.WebApi/Infrastructure/Models/TestModel.cs:
--------------------------------------------------------------------------------
1 | namespace ValidationExtension.Tests.WebApi.Infrastructure.Models;
2 |
3 | public sealed class TestModel
4 | {
5 | public int Id { get; set; }
6 |
7 | public string Name { get; set; }
8 | }
9 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests.WebApi/Infrastructure/Models/TestResponse.cs:
--------------------------------------------------------------------------------
1 | using TechBuddy.Extension.Validation.Infrastructure.Models.ResponseModels;
2 |
3 | public class TestResponse : BaseValidationErrorResponseModel
4 | {
5 | public int HttpStatusCode { get; set; }
6 | }
7 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests.WebApi/Infrastructure/TestModelProvider.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc.ModelBinding;
2 | using System.Net;
3 | using TechBuddy.Extension.Validation.Infrastructure.Models.ModelProviders;
4 |
5 | public class TestModelProvider : IDefaultModelProvider
6 | {
7 | public object GetModel(ModelStateDictionary.ValueEnumerable modelStateValues)
8 | {
9 | return new TestResponse()
10 | {
11 | Errors = modelStateValues.SelectMany(i => i.Errors).Select(i => string.Join(Environment.NewLine, i.ErrorMessage)).ToList(),
12 | HttpStatusCode = (int)HttpStatusCode.BadRequest
13 | };
14 | }
15 | }
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests.WebApi/Infrastructure/Validators/TestValidator.cs:
--------------------------------------------------------------------------------
1 | using FluentValidation;
2 | using ValidationExtension.Tests.WebApi.Infrastructure.Models;
3 |
4 | namespace ValidationExtension.Tests.WebApi.Infrastructure.Validators;
5 | public class TestValidator : AbstractValidator
6 | {
7 | public TestValidator()
8 | {
9 | RuleFor(i => i.Id).GreaterThan(0).WithMessage("{PropertyName} cannot be zero!");
10 | RuleFor(i => i.Name).MinimumLength(3).WithMessage("{PropertyName} must be at least {MinLength} character");
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests.WebApi/Program.cs:
--------------------------------------------------------------------------------
1 | using TechBuddy.Extension.Validation.Extensions;
2 |
3 | var builder = WebApplication.CreateBuilder(args);
4 |
5 | // Add services to the container.
6 |
7 | builder.Services.AddControllers();
8 |
9 |
10 | builder.Services.AddTechBuddyValidator(config =>
11 | {
12 | //config.UseModelProvider();
13 | });
14 |
15 | // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
16 | builder.Services.AddEndpointsApiExplorer();
17 | builder.Services.AddSwaggerGen();
18 |
19 |
20 | var app = builder.Build();
21 |
22 | // Configure the HTTP request pipeline.
23 | if (app.Environment.IsDevelopment())
24 | {
25 | app.UseSwagger();
26 | app.UseSwaggerUI();
27 | }
28 |
29 | app.UseHttpsRedirection();
30 |
31 | app.UseAuthorization();
32 |
33 | app.MapControllers();
34 |
35 | app.Run();
36 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests.WebApi/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:60056",
8 | "sslPort": 44395
9 | }
10 | },
11 | "profiles": {
12 | "ValidationExtension.Tests.WebApi": {
13 | "commandName": "Project",
14 | "dotnetRunMessages": true,
15 | "launchBrowser": true,
16 | "launchUrl": "swagger",
17 | "applicationUrl": "https://localhost:7249;http://localhost:5179",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development"
20 | }
21 | },
22 | "IIS Express": {
23 | "commandName": "IISExpress",
24 | "launchBrowser": true,
25 | "launchUrl": "swagger",
26 | "environmentVariables": {
27 | "ASPNETCORE_ENVIRONMENT": "Development"
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests.WebApi/ValidationExtension.Tests.WebApi.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 |
7 | false
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests.WebApi/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests.WebApi/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "AllowedHosts": "*"
9 | }
10 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests/Infrastructure/Controllers/TestController.cs:
--------------------------------------------------------------------------------
1 | using FluentValidation.AspNetCore;
2 | using Microsoft.AspNetCore.Mvc;
3 |
4 | namespace ValidationExtension.Tests.Infrastructure.Controllers;
5 |
6 | [ApiController]
7 | [Route("api/[controller]")]
8 | public class TestController : ControllerBase
9 | {
10 | [HttpGet()]
11 | public ActionResult Get()
12 | {
13 | return Ok("OK");
14 | }
15 |
16 | [HttpPost()]
17 | public ActionResult Post([FromBody] TestModel testModel)
18 | {
19 | return Ok(testModel.Name);
20 | }
21 |
22 | [HttpPost("SkipValidation")]
23 | public ActionResult PostSkipValidation([CustomizeValidator(Skip = true)][FromBody] TestModel testModel)
24 | {
25 | return Ok(testModel.Name);
26 | }
27 | }
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests/Infrastructure/Helpers/ModelProviders/TestModelProvider.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc.ModelBinding;
2 | using System.Net;
3 | using TechBuddy.Extension.Validation.Infrastructure.Models.ModelProviders;
4 |
5 | namespace ValidationExtension.Tests.Infrastructure.Helpers.ModelProviders;
6 | internal class TestModelProvider : IDefaultModelProvider
7 | {
8 | public object GetModel(ModelStateDictionary.ValueEnumerable modelStateValues)
9 | {
10 | return new TestResponse()
11 | {
12 | Errors = modelStateValues.SelectMany(i => i.Errors).Select(i => string.Join(Environment.NewLine, i.ErrorMessage)).ToList(),
13 | HttpStatusCode = (int)HttpStatusCode.BadRequest
14 | };
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests/Infrastructure/Helpers/Validators/TestValidator.cs:
--------------------------------------------------------------------------------
1 | namespace ValidationExtension.Tests.Helpers.Validators;
2 | public sealed class TestValidator : AbstractValidator
3 | {
4 | public TestValidator()
5 | {
6 | RuleFor(i => i.Id).GreaterThan(0).WithMessage("{PropertyName} cannot be zero!");
7 | RuleFor(i => i.Name).MinimumLength(3).WithMessage("{PropertyName} must be at least {MinLenght} character");
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests/Infrastructure/Helpers/Validators/TestValidatorV2.cs:
--------------------------------------------------------------------------------
1 | namespace ValidationExtension.Tests.Helpers.Validators;
2 |
3 | public sealed class TestValidatorV2 : AbstractValidator
4 | {
5 | public TestValidatorV2()
6 | {
7 | RuleFor(i => i.Id).GreaterThan(0).WithMessage("{PropertyName} cannot be zero!");
8 | RuleFor(i => i.FullName).MinimumLength(3).WithMessage("{PropertyName} must be at least {MinLenght} character");
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests/Infrastructure/Models/ResponseModels/TestResponse.cs:
--------------------------------------------------------------------------------
1 | internal class TestResponse : BaseValidationErrorResponseModel
2 | {
3 | public int HttpStatusCode { get; set; }
4 | }
5 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests/Infrastructure/Models/TestModel.cs:
--------------------------------------------------------------------------------
1 | namespace ValidationExtension.Tests.Infrastructure.Models;
2 | public sealed class TestModel
3 | {
4 | public int Id { get; set; }
5 |
6 | public string Name { get; set; }
7 | }
8 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests/Infrastructure/Models/TestModelV2.cs:
--------------------------------------------------------------------------------
1 | namespace ValidationExtension.Tests.Infrastructure.Models;
2 |
3 | public sealed class TestModelV2
4 | {
5 | public int Id { get; set; }
6 |
7 | public string FullName { get; set; }
8 | }
9 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests/Tests/HttpRequestTests.cs:
--------------------------------------------------------------------------------
1 | using TechBuddy.Extension.Validation.Extensions;
2 | using TechBuddy.Extensions.Tests.Common.TestCommon.Builders;
3 | using TechBuddy.Extensions.Tests.Common.TestCommon.Constants;
4 |
5 | namespace ValidationExtension.Tests.Tests;
6 | public sealed class HttpRequestTests
7 | {
8 | [Test]
9 | public async Task Validate_WithValidModel_ShouldValidate()
10 | {
11 | // Arrange
12 | var model = new TestModel()
13 | {
14 | Id = 1,
15 | Name = "Test"
16 | };
17 |
18 | var request = HttpRequestBuilder
19 | .CreateRequest()
20 | .SetBody(JsonSerializer.Serialize(model, GeneralConstants.JsonOptions))
21 | .ToRequest();
22 |
23 | // Act
24 | var result = await request.ValidateAsync();
25 |
26 |
27 | // Assert
28 | result.IsValid.Should().BeTrue();
29 | result.Model.Should().BeEquivalentTo(model);
30 | result.Errors.Should().BeNullOrEmpty();
31 | }
32 |
33 | [Test]
34 | public async Task Validate_WithInvalidPropertyV1_ShouldNotValidate()
35 | {
36 | // Arrange
37 | var model = new TestModel()
38 | {
39 | Id = 0,
40 | Name = "Test"
41 | };
42 |
43 | var request = HttpRequestBuilder
44 | .CreateRequest()
45 | .SetBody(JsonSerializer.Serialize(model, GeneralConstants.JsonOptions))
46 | .ToRequest();
47 |
48 | // Act
49 | var result = await request.ValidateAsync();
50 |
51 |
52 | // Assert
53 | result.IsValid.Should().BeFalse();
54 | result.Model.Should().BeEquivalentTo(model);
55 | result.Errors.Should().NotBeNullOrEmpty().And.HaveCount(1);
56 | }
57 |
58 | [Test]
59 | public async Task Validate_WithInvalidPropertyV2_ShouldNotValidate()
60 | {
61 | // Arrange
62 | var model = new TestModel()
63 | {
64 | Id = 0,
65 | Name = ""
66 | };
67 |
68 | var request = HttpRequestBuilder
69 | .CreateRequest()
70 | .SetBody(JsonSerializer.Serialize(model, GeneralConstants.JsonOptions))
71 | .ToRequest();
72 |
73 | // Act
74 | var result = await request.ValidateAsync();
75 |
76 |
77 | // Assert
78 | result.IsValid.Should().BeFalse();
79 | result.Model.Should().BeEquivalentTo(model);
80 | result.Errors.Should().NotBeNullOrEmpty().And.HaveCount(2);
81 | }
82 |
83 | [Test]
84 | public async Task Validate_WhenHttpRequestNull_ShouldThrowArgumentNullException()
85 | {
86 | // Arrange
87 | HttpRequest request = null;
88 |
89 | // Act
90 | var result = async () => await request.ValidateAsync();
91 |
92 | // Assert
93 | await result.Should().ThrowAsync();
94 | }
95 |
96 | [Test]
97 | public async Task Validate_WhenHttpRequestBodyEmpty_ShouldThrowArgumentNullException()
98 | {
99 | // Arrange
100 | var request = HttpRequestBuilder
101 | .CreateRequest()
102 | .ToRequest();
103 |
104 | // Act
105 | var result = async () => await request.ValidateAsync();
106 |
107 | // Assert
108 | await result.Should().ThrowAsync();
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests/Tests/ValidationDIExtensionsTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using System.Reflection;
3 | using TechBuddy.Extension.Validation.Extensions;
4 | using TechBuddy.Extension.Validation.Infrastructure.Models.ModelProviders;
5 | using ValidationExtension.Tests.Infrastructure.Helpers.ModelProviders;
6 |
7 | namespace ValidationExtension.Tests.Tests;
8 | public sealed class ValidationDIExtensionsTests
9 | {
10 | #region AddTechBuddyValidatorFromAssemblyContaining Tests
11 |
12 | [Test]
13 | public void AddValidatorFromAssemblyContaining_ShouldRegisterSpecificValidator()
14 | {
15 | // Arrange
16 | var services = new ServiceCollection();
17 |
18 | // Action
19 | services.AddTechBuddyValidatorFromAssemblyContaining();
20 |
21 | var provider = services.BuildServiceProvider();
22 | var validator = provider.GetService();
23 |
24 | // Assert
25 | validator.Should().NotBeNull();
26 | }
27 |
28 | [Test]
29 | public void AddValidatorFromAssemblyContaining_ShouldRegisterAllValidators()
30 | {
31 | // Arrange
32 | var services = new ServiceCollection();
33 |
34 | // Action
35 | services.AddTechBuddyValidatorFromAssemblyContaining();
36 |
37 | var provider = services.BuildServiceProvider();
38 | var validator = provider.GetService();
39 | var validator2 = provider.GetService();
40 |
41 | // Assert
42 | validator.Should().NotBeNull();
43 | validator2.Should().NotBeNull();
44 | }
45 |
46 | #endregion
47 |
48 | #region AddTechBuddyValidator Tests
49 |
50 | [Test]
51 | public void AddValidator_WithNoAssembly_ShouldRegisterSpecificValidator()
52 | {
53 | // Arrange
54 | var services = new ServiceCollection();
55 |
56 | // Action
57 | services.AddTechBuddyValidator();
58 |
59 | var provider = services.BuildServiceProvider();
60 | var validator = provider.GetService();
61 |
62 | // Assert
63 | validator.Should().NotBeNull();
64 | }
65 |
66 | [Test]
67 | public void AddValidator_WithNoAssembly_ShouldRegisterAllValidator()
68 | {
69 | // Arrange
70 | var services = new ServiceCollection();
71 |
72 | // Action
73 | services.AddTechBuddyValidator();
74 |
75 | var provider = services.BuildServiceProvider();
76 | var validator = provider.GetService();
77 | var validator2 = provider.GetService();
78 |
79 | // Assert
80 | validator.Should().NotBeNull();
81 | validator2.Should().NotBeNull();
82 | }
83 |
84 | [Test]
85 | public void AddValidator_WithAssembly_ShouldRegisterSpecificValidator()
86 | {
87 | // Arrange
88 | var services = new ServiceCollection();
89 | var assembly = Assembly.GetExecutingAssembly();
90 |
91 | // Action
92 | services.AddTechBuddyValidatorFromAssembly(assembly);
93 |
94 | var provider = services.BuildServiceProvider();
95 | var validator = provider.GetService();
96 |
97 | // Assert
98 | validator.Should().NotBeNull();
99 | }
100 |
101 | [Test]
102 | public void AddValidator_WithAssembly_ShouldRegisterAllValidator()
103 | {
104 | // Arrange
105 | var services = new ServiceCollection();
106 | var assembly = Assembly.GetExecutingAssembly();
107 |
108 | // Action
109 | services.AddTechBuddyValidatorFromAssembly(assembly);
110 |
111 | var provider = services.BuildServiceProvider();
112 | var validator = provider.GetService();
113 | var validator2 = provider.GetService();
114 |
115 | // Assert
116 | validator.Should().NotBeNull();
117 | validator2.Should().NotBeNull();
118 | }
119 |
120 | [Test]
121 | public void AddValidatorWithConfig_WithNoAssembly_ShouldRegisterSpecificValidator()
122 | {
123 | // Arrange
124 | var services = new ServiceCollection();
125 |
126 | // Action
127 | services.AddTechBuddyValidator(conf =>
128 | {
129 | conf.UseModelProvider();
130 | });
131 |
132 | var provider = services.BuildServiceProvider();
133 | var modelProvider = provider.GetService();
134 |
135 | // Assert
136 | modelProvider.Should().BeOfType();
137 | }
138 |
139 | #endregion
140 | }
141 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests/Tests/ValidationErrorResponseFactoryTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 |
3 | namespace ValidationExtension.Tests.Tests;
4 | public sealed class ValidationErrorResponseFactoryTests
5 | {
6 | [Test]
7 | public void CreateModel_WithMessage_ShouldContainMessage()
8 | {
9 | // Arrange
10 | var message = "TestMessage";
11 |
12 | // Action
13 | var model = ValidationErrorResponseFactory.CreateModel(message);
14 |
15 | // Assert
16 | model.Errors.Should().Contain(message);
17 | model.Errors.Should().HaveCount(1);
18 | }
19 |
20 |
21 | [Test]
22 | public void CreateModel_WithListOfMessage_ShouldContainMessages()
23 | {
24 | // Arrange
25 | var messages = new List()
26 | {
27 | "TestMessage1",
28 | "TestMessage2"
29 | };
30 |
31 | // Action
32 | var model = ValidationErrorResponseFactory.CreateModel(messages);
33 |
34 | // Assert
35 | model.Errors.Should().BeEquivalentTo(messages);
36 | model.Errors.Should().HaveCount(messages.Count);
37 | }
38 |
39 |
40 | [Test]
41 | public void CreateActionResult_WithMessage_ShouldReturnBadRequestObject()
42 | {
43 | // Arrange
44 | var message = "TestMessage";
45 |
46 | // Action
47 | var actionResult = ValidationErrorResponseFactory.CreateActionResult(message);
48 |
49 | // Assert
50 | actionResult.Should().BeOfType();
51 | }
52 |
53 | [Test]
54 | public void CreateActionResult_WithMessage_ShouldContainMessageAsObject()
55 | {
56 | // Arrange
57 | var message = "TestMessage";
58 |
59 | // Action
60 | var actionResult = ValidationErrorResponseFactory.CreateActionResult(message);
61 |
62 | // Assert
63 | var value = (actionResult as BadRequestObjectResult).Value;
64 |
65 | value.Should().BeOfType();
66 | value.As().Errors.Contains(message);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests/Tests/ValidationErrorResponseModelTests.cs:
--------------------------------------------------------------------------------
1 | namespace ValidationExtension.Tests.Tests;
2 | public sealed class ValidationErrorResponseModelTests
3 | {
4 | [Test]
5 | public void WithMessage_ShouldContainMessage()
6 | {
7 | // Arrange
8 | var message = "TestMessage";
9 |
10 | // Action
11 | var model = new DefaultValidationErrorResponseModel(message);
12 |
13 | // Assert
14 | model.Errors.Should().Contain(message);
15 | model.Errors.Should().HaveCount(1);
16 | }
17 |
18 |
19 | [Test]
20 | public void WithListOfMessage_ShouldContainMessages()
21 | {
22 | // Arrange
23 | var messages = new List()
24 | {
25 | "TestMessage1",
26 | "TestMessage2"
27 | };
28 |
29 | // Action
30 | var model = new DefaultValidationErrorResponseModel(messages);
31 |
32 | // Assert
33 | model.Errors.Should().BeEquivalentTo(messages);
34 | model.Errors.Should().HaveCount(messages.Count);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests/Tests/ValidationResultModelTests.cs:
--------------------------------------------------------------------------------
1 | using TechBuddy.Extension.Validation.Infrastructure.Models;
2 |
3 | namespace ValidationExtension.Tests.Tests;
4 | public sealed class ValidationResultModelTests
5 | {
6 | [Test]
7 | public void ValidationResultModel_IsValid_ShouldBeFalseAsDefault()
8 | {
9 | // Arrange
10 | var model = new ValidationResultModel();
11 |
12 |
13 | // Assert
14 | model.IsValid.Should().BeFalse();
15 | }
16 |
17 | [Test]
18 | public void ValidationResultModel_ErrorMessages_ShouldBeNullAsDefault()
19 | {
20 | // Arrange
21 | var model = new ValidationResultModel();
22 |
23 |
24 | // Assert
25 | model.Errors.Should().BeNull();
26 | }
27 |
28 | [Test]
29 | public void ValidationResultModel_Model_ShouldBeNullAsDefault()
30 | {
31 | // Arrange
32 | var model = new ValidationResultModel();
33 |
34 |
35 | // Assert
36 | model.Model.Should().BeNull();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests/Tests/ValidationTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.AspNetCore.Hosting;
3 | using Microsoft.AspNetCore.TestHost;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using System.Net.Http.Json;
6 | using TechBuddy.Extension.Validation.Extensions;
7 | using TechBuddy.Extensions.Tests.Common.TestCommon.Builders;
8 | using ValidationExtension.Tests.Infrastructure.Helpers.ModelProviders;
9 |
10 | namespace ValidationExtension.Tests.Tests;
11 | internal class ValidationTests
12 | {
13 | private TestServer defaultVersionTestServer;
14 |
15 | [OneTimeSetUp]
16 | public void SetUp()
17 | {
18 | defaultVersionTestServer = GetTestServerWithDefaultConfig();
19 | }
20 |
21 | [Test]
22 | public async Task TestController_WithTestModel_ShouldReturnModelName()
23 | {
24 | // Arrange
25 | var client = defaultVersionTestServer.CreateClient();
26 | var testModel = new TestModel()
27 | {
28 | Id = 1,
29 | Name = "Test"
30 | };
31 |
32 | var request = HttpRequestBuilder
33 | .CreateRequest("api/Test", HttpMethod.Post)
34 | .SetBody(testModel)
35 | .ToRequestMessage();
36 |
37 | // Action
38 | HttpResponseMessage response = await client.SendAsync(request);
39 | var responseContent = await response.Content.ReadAsStringAsync();
40 |
41 | //Assert
42 | responseContent.Should().Be(testModel.Name);
43 | }
44 |
45 | [Test]
46 | public async Task TestController_WithInvalidTestModel_ShouldReturnBadRequest()
47 | {
48 | // Arrange
49 | var client = defaultVersionTestServer.CreateClient();
50 | var testModel = new TestModel()
51 | {
52 | Id = 0,
53 | Name = "Test"
54 | };
55 |
56 | var request = HttpRequestBuilder
57 | .CreateRequest("api/Test", HttpMethod.Post)
58 | .SetBody(testModel)
59 | .ToRequestMessage();
60 |
61 | // Action
62 | HttpResponseMessage response = await client.SendAsync(request);
63 |
64 | //Assert
65 | response.IsSuccessStatusCode.Should().BeFalse();
66 | response.StatusCode.Should().Be(System.Net.HttpStatusCode.BadRequest);
67 | }
68 |
69 | [Test]
70 | public async Task TestController_WithModelProvider_ShouldReturnTestResponse()
71 | {
72 | // Arrange
73 | var client = GetTestServerModelProvider().CreateClient();
74 | var testModel = new TestModel()
75 | {
76 | Id = 0,
77 | Name = "Test"
78 | };
79 |
80 | var request = HttpRequestBuilder
81 | .CreateRequest("api/Test", HttpMethod.Post)
82 | .SetBody(testModel)
83 | .ToRequestMessage();
84 |
85 | // Action
86 | HttpResponseMessage response = await client.SendAsync(request);
87 | var responseContent = await response.Content.ReadFromJsonAsync();
88 |
89 | //Assert
90 | response.IsSuccessStatusCode.Should().BeFalse();
91 | responseContent.Should().NotBeNull();
92 | }
93 |
94 |
95 | [Test]
96 | public async Task TestController_WithSkipValidationAndInvalidModel_ShouldSkip()
97 | {
98 | // Arrange
99 | var client = GetTestServerModelProvider().CreateClient();
100 | var testModel = new TestModel()
101 | {
102 | Id = 0,
103 | Name = "Test"
104 | };
105 |
106 | var request = HttpRequestBuilder
107 | .CreateRequest("api/Test/SkipValidation", HttpMethod.Post)
108 | .SetBody(testModel)
109 | .ToRequestMessage();
110 |
111 | // Action
112 | HttpResponseMessage response = await client.SendAsync(request);
113 | var responseContent = await response.Content.ReadAsStringAsync();
114 |
115 | //Assert
116 | responseContent.Should().Be(testModel.Name);
117 | }
118 |
119 | [Test]
120 | public async Task TestController_WithSkipValidationAndValidModel_ShouldReturnModelName()
121 | {
122 | // Arrange
123 | var client = GetTestServerModelProvider().CreateClient();
124 | var testModel = new TestModel()
125 | {
126 | Id = 1,
127 | Name = "Test"
128 | };
129 |
130 | var request = HttpRequestBuilder
131 | .CreateRequest("api/Test/SkipValidation", HttpMethod.Post)
132 | .SetBody(testModel)
133 | .ToRequestMessage();
134 |
135 | // Action
136 | HttpResponseMessage response = await client.SendAsync(request);
137 | var responseContent = await response.Content.ReadAsStringAsync();
138 |
139 | //Assert
140 | responseContent.Should().Be(testModel.Name);
141 | }
142 |
143 |
144 |
145 | private static TestServer GetTestServerWithDefaultConfig()
146 | {
147 | var hostBuilder = new WebHostBuilder()
148 | .ConfigureServices(services =>
149 | {
150 | services.AddMvc(i => i.EnableEndpointRouting = false);
151 | services.AddTechBuddyValidator();
152 | })
153 | .Configure(app =>
154 | {
155 | app.UseMvc();
156 | });
157 |
158 | return new TestServer(hostBuilder);
159 | }
160 |
161 | private static TestServer GetTestServerModelProvider()
162 | {
163 | var hostBuilder = new WebHostBuilder()
164 | .ConfigureServices(services =>
165 | {
166 | services.AddMvc(i => i.EnableEndpointRouting = false);
167 | services.AddTechBuddyValidator(conf => conf.UseModelProvider());
168 | })
169 | .Configure(app =>
170 | {
171 | app.UseMvc();
172 | });
173 |
174 | return new TestServer(hostBuilder);
175 | }
176 |
177 | }
178 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests/Usings.cs:
--------------------------------------------------------------------------------
1 | global using FluentAssertions;
2 | global using FluentValidation;
3 | global using Microsoft.AspNetCore.Http;
4 | global using NUnit.Framework;
5 | global using System.Text.Json;
6 | global using TechBuddy.Extension.Validation.Infrastructure.Factories;
7 | global using TechBuddy.Extension.Validation.Infrastructure.Models.ResponseModels;
8 | global using ValidationExtension.Tests.Helpers.Validators;
9 | global using ValidationExtension.Tests.Infrastructure.Models;
10 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension.Tests/ValidationExtension.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 |
7 | false
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | all
20 | runtime; build; native; contentfiles; analyzers; buildtransitive
21 |
22 |
23 | all
24 | runtime; build; native; contentfiles; analyzers; buildtransitive
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension/Extensions/HttpRequestExtensions.cs:
--------------------------------------------------------------------------------
1 | using FluentValidation;
2 | using Microsoft.AspNetCore.Http;
3 | using System.Text;
4 | using System.Text.Json;
5 | using TechBuddy.Extension.Validation.Infrastructure.Models;
6 |
7 | namespace TechBuddy.Extension.Validation.Extensions;
8 |
9 | ///
10 | /// The HttpRequest Extensions
11 | ///
12 | public static class HttpRequestExtensions
13 | {
14 | ///
15 | /// Validates the object from http request body if it is , with provided
16 | ///
17 | /// The model which the body object will be compared to
18 | /// The validator (FluentValidator used)
19 | /// The HttpRequest to get the body object
20 | ///
21 | public static async Task> ValidateAsync(this HttpRequest req)
22 | where TModel : new()
23 | where TValidator : IValidator
24 | {
25 | ArgumentNullException.ThrowIfNull(req);
26 |
27 | if (req.Body.Length <= 0)
28 | throw new ArgumentException("Request body cannot be empty!");
29 |
30 | var validator = Activator.CreateInstance();
31 |
32 | var result = new ValidationResultModel
33 | {
34 | Model = await req.ReadBodyAsync()
35 | };
36 |
37 | var validatorResult = await validator.ValidateAsync(result.Model);
38 |
39 | result.IsValid = validatorResult.IsValid;
40 |
41 | if (!result.IsValid)
42 | result.Errors = validatorResult.Errors.Select(i => i.ErrorMessage).ToList();
43 |
44 | return result;
45 | }
46 |
47 |
48 | ///
49 | /// Reads and returns the generic body object from httprequest by serializing to provided generic type
50 | ///
51 | /// The type for object to be serialized
52 | /// The http request to get the body object
53 | /// The generic object
54 | private static async Task ReadBodyAsync(this HttpRequest req)
55 | {
56 | req.EnableBuffering();
57 | req.Body.Seek(0, SeekOrigin.Begin);
58 | using var reader = new StreamReader(req.Body, Encoding.UTF8, leaveOpen: true);
59 | var jsonBody = await reader.ReadToEndAsync();
60 | req.Body.Position = 0;
61 |
62 | return JsonSerializer.Deserialize(jsonBody);
63 | }
64 |
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension/Extensions/ValidationDependencyInjectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using FluentValidation;
2 | using FluentValidation.AspNetCore;
3 | using Microsoft.AspNetCore.Mvc;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using System.Reflection;
6 | using TechBuddy.Extension.Validation.Infrastructure.ActionFilters;
7 | using TechBuddy.Extension.Validation.Infrastructure.Models.ConfigModels;
8 | using TechBuddy.Extension.Validation.Infrastructure.Models.ModelProviders;
9 |
10 | namespace TechBuddy.Extension.Validation.Extensions;
11 |
12 | ///
13 | /// The extension class for IServiceCollection to inject the Validators
14 | ///
15 | public static class ValidationDependencyInjectionExtensions
16 | {
17 |
18 | ///
19 | /// Registers all the classes derived by IValidator (AbstractValidator included) by scanning all the classes in provided Assembly
20 | ///
21 | /// The ServiceCollection
22 | /// The assembly of your API or where Validators are
23 | /// retuns ServiceCollection
24 | public static IServiceCollection AddTechBuddyValidatorFromAssembly(this IServiceCollection services,
25 | Assembly assembly)
26 | {
27 | ConfigureModelProvider(services);
28 |
29 | services.AddFluentValidationAutoValidation();
30 | services.AddValidatorsFromAssembly(assembly);
31 |
32 | return services;
33 | }
34 |
35 |
36 | ///
37 | /// Registers all the classes derived by IValidator (AbstractValidator included) by scanning all the classes in provided Assembly
38 | ///
39 | /// The ServiceCollection
40 | /// The assembly of your API or where Validators are
41 | /// The ValidationExtensionConfig
42 | /// retuns ServiceCollection
43 | public static IServiceCollection AddTechBuddyValidatorFromAssembly(this IServiceCollection services,
44 | Assembly assembly,
45 | Action configAction)
46 | {
47 | var config = new ValidationExtensionConfig();
48 | configAction.Invoke(config);
49 |
50 | ConfigureModelProvider(services, config);
51 |
52 | services.AddFluentValidationAutoValidation();
53 | services.AddValidatorsFromAssembly(assembly);
54 |
55 | return services;
56 | }
57 |
58 |
59 | ///
60 | /// Registers all the classes derived by IValidator (AbstractValidator included) by scanning all the classes in Calling Assembly
61 | ///
62 | /// The ServiceCollection
63 | /// retuns ServiceCollection
64 | public static IServiceCollection AddTechBuddyValidator(this IServiceCollection services)
65 | {
66 | var assembly = Assembly.GetCallingAssembly(); // Get all assemblies from the project where this method is called
67 |
68 | ConfigureModelProvider(services);
69 |
70 | services.AddFluentValidationAutoValidation();
71 | services.AddValidatorsFromAssembly(assembly);
72 |
73 | return services;
74 | }
75 |
76 | ///
77 | /// Registers all the classes derived by IValidator (AbstractValidator included) by scanning all the classes in Calling Assembly
78 | ///
79 | /// The ServiceCollection
80 | /// The ValidationExtensionConfig
81 | /// retuns ServiceCollection
82 | public static IServiceCollection AddTechBuddyValidator(this IServiceCollection services,
83 | Action configAction)
84 | {
85 | var config = new ValidationExtensionConfig();
86 | configAction(config); // Fill the config
87 |
88 | ConfigureModelProvider(services, config);
89 |
90 | var assembly = Assembly.GetCallingAssembly(); // Get all assemblies from the project where this method is called
91 |
92 | services.AddFluentValidationAutoValidation();
93 | services.AddValidatorsFromAssembly(assembly);
94 |
95 | return services;
96 | }
97 |
98 |
99 |
100 | ///
101 | /// Registers all the classes derived by IValidator (AbstractValidator included) by scanning all the classes in the Assembly where is
102 | ///
103 | /// The type of Validator
104 | /// The ServiceCollection
105 | /// retuns ServiceCollection
106 | public static IServiceCollection AddTechBuddyValidatorFromAssemblyContaining(this IServiceCollection services)
107 | where T : IValidator
108 | {
109 | ConfigureModelProvider(services);
110 |
111 | services.AddFluentValidationAutoValidation();
112 | services.AddValidatorsFromAssemblyContaining();
113 |
114 | return services;
115 | }
116 |
117 |
118 | ///
119 | /// Registers all the classes derived by IValidator (AbstractValidator included) by scanning all the classes in the Assembly where is
120 | ///
121 | /// The type of Validator
122 | /// The ServiceCollection
123 | /// The ValidationExtensionConfig
124 | /// retuns ServiceCollection
125 | public static IServiceCollection AddTechBuddyValidatorFromAssemblyContaining(this IServiceCollection services,
126 | Action configAction)
127 | where T : IValidator
128 | {
129 | var config = new ValidationExtensionConfig();
130 | configAction(config);
131 |
132 | ConfigureModelProvider(services, config);
133 |
134 | services.AddFluentValidationAutoValidation();
135 | services.AddValidatorsFromAssemblyContaining();
136 |
137 | return services;
138 | }
139 |
140 |
141 |
142 | private static void ConfigureModelProvider(IServiceCollection services, ValidationExtensionConfig config = null)
143 | {
144 | services.Configure(config =>
145 | {
146 | config.SuppressModelStateInvalidFilter = true;
147 | });
148 |
149 | services.AddControllers(config =>
150 | {
151 | config.Filters.Add();
152 | });
153 |
154 | if (config is not null)
155 | {
156 | services.AddSingleton(config);
157 | }
158 |
159 | IDefaultModelProvider modelProvider = config?.ModelProvider ?? new DefaultModelProvider();
160 | services.AddTransient(i => modelProvider);
161 | }
162 |
163 | }
164 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension/Infrastructure/ActionFilters/ValidateModelStateActionFilter.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Microsoft.AspNetCore.Mvc.Filters;
3 | using TechBuddy.Extension.Validation.Infrastructure.Models.ModelProviders;
4 |
5 | namespace TechBuddy.Extension.Validation.Infrastructure.ActionFilters;
6 |
7 | ///
8 | /// The an ActionFilter for NET which handles InValid ModelState and returns the response by calling
9 | ///
10 | public class ValidateModelStateActionFilter : IAsyncActionFilter
11 | {
12 | private readonly IDefaultModelProvider defaultModelProvider;
13 |
14 | ///
15 | /// Initiates the
16 | ///
17 | /// The model provider
18 | public ValidateModelStateActionFilter(IDefaultModelProvider defaultModelProvider)
19 | {
20 | this.defaultModelProvider = defaultModelProvider;
21 | }
22 |
23 | ///
24 | public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
25 | {
26 | if (context.ModelState.IsValid)
27 | {
28 | await next();
29 | return;
30 | }
31 |
32 | var model = defaultModelProvider.GetModel(context.ModelState.Values);
33 |
34 | context.Result = new BadRequestObjectResult(model);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension/Infrastructure/Factories/ValidationErrorResponseFactory.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using TechBuddy.Extension.Validation.Infrastructure.Models.ResponseModels;
3 |
4 | namespace TechBuddy.Extension.Validation.Infrastructure.Factories;
5 |
6 | ///
7 | /// The ValidationErrorReponseFactory class
8 | ///
9 | public sealed class ValidationErrorResponseFactory
10 | {
11 | ///
12 | /// Gets BadRequestObjectResults that has the body as with the provided
13 | ///
14 | /// The validation message
15 | /// returns
16 | public static IActionResult CreateActionResult(string message)
17 | {
18 | var model = new DefaultValidationErrorResponseModel(message);
19 |
20 | return new BadRequestObjectResult(model);
21 | }
22 |
23 | ///
24 | /// Gets with
25 | ///
26 | /// The validation message
27 | /// returns
28 | public static DefaultValidationErrorResponseModel CreateModel(string message)
29 | {
30 | return new DefaultValidationErrorResponseModel(message);
31 | }
32 |
33 | ///
34 | /// Gets with as validation error messages
35 | ///
36 | /// The list of validation message
37 | /// returns
38 | public static DefaultValidationErrorResponseModel CreateModel(IEnumerable messages)
39 | {
40 | return new DefaultValidationErrorResponseModel(messages?.ToList());
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension/Infrastructure/Models/ConfigModels/ValidationExtensionConfig.cs:
--------------------------------------------------------------------------------
1 | using TechBuddy.Extension.Validation.Infrastructure.Models.ModelProviders;
2 |
3 | namespace TechBuddy.Extension.Validation.Infrastructure.Models.ConfigModels;
4 | ///
5 | /// The ValidationExtensionConfig model
6 | ///
7 | public class ValidationExtensionConfig
8 | {
9 | internal IDefaultModelProvider ModelProvider { get; set; }
10 |
11 |
12 | ///
13 | /// Adds an instance of the model that derived from
14 | ///
15 | /// The model
16 | public void UseModelProvider(T modelProvider)
17 | where T : IDefaultModelProvider
18 | {
19 | ModelProvider = modelProvider;
20 | }
21 |
22 | ///
23 | /// Sets the model that derived from
24 | ///
25 | ///
26 | public void UseModelProvider()
27 | where T : IDefaultModelProvider
28 | {
29 | ModelProvider = Activator.CreateInstance();
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension/Infrastructure/Models/ModelProviders/DefaultModelProvider.cs:
--------------------------------------------------------------------------------
1 | using TechBuddy.Extension.Validation.Infrastructure.Models.ResponseModels;
2 | using static Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary;
3 |
4 | namespace TechBuddy.Extension.Validation.Infrastructure.Models.ModelProviders;
5 | ///
6 | internal class DefaultModelProvider : IDefaultModelProvider
7 | {
8 |
9 | ///
10 | public object GetModel(ValueEnumerable modelStateValues)
11 | {
12 | var errors = modelStateValues.Where(i => i.Errors.Count > 0)
13 | .SelectMany(i => i.Errors)
14 | .Select(i => i.ErrorMessage);
15 |
16 | return new DefaultValidationErrorResponseModel(errors);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension/Infrastructure/Models/ModelProviders/IDefaultModelProvider.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc.ModelBinding;
2 |
3 | namespace TechBuddy.Extension.Validation.Infrastructure.Models.ModelProviders;
4 |
5 | ///
6 | /// The default model provider interface that promise to return an object for ResponseBody
7 | ///
8 | public interface IDefaultModelProvider
9 | {
10 | ///
11 | /// Get the object model to be writting as ResponseBody.
12 | ///
13 | /// The argument in which you can find validation exception details
14 | /// returns ResponseBody object
15 | object GetModel(ModelStateDictionary.ValueEnumerable modelStateValues);
16 | }
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension/Infrastructure/Models/ResponseModels/BaseValidationErrorResponseModel.cs:
--------------------------------------------------------------------------------
1 | namespace TechBuddy.Extension.Validation.Infrastructure.Models.ResponseModels;
2 |
3 | ///
4 | /// BaseInvalidResponse Model
5 | ///
6 | public abstract class BaseValidationErrorResponseModel
7 | {
8 | ///
9 | /// The parameterless contsturctor
10 | ///
11 | public BaseValidationErrorResponseModel()
12 | {
13 |
14 | }
15 |
16 |
17 | ///
18 | /// The constructor that sets the
19 | ///
20 | ///
21 | protected BaseValidationErrorResponseModel(List errors)
22 | {
23 | Errors = errors;
24 | }
25 |
26 | ///
27 | /// List of exceptions occured
28 | ///
29 | public List Errors { get; set; }
30 | }
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension/Infrastructure/Models/ResponseModels/DefaultValidationErrorResponseModel.cs:
--------------------------------------------------------------------------------
1 | namespace TechBuddy.Extension.Validation.Infrastructure.Models.ResponseModels;
2 |
3 | ///
4 | /// The default ValidationErrorResponseModel
5 | ///
6 | public class DefaultValidationErrorResponseModel : BaseValidationErrorResponseModel
7 | {
8 | ///
9 | /// The constructor
10 | ///
11 | /// The exception message
12 | public DefaultValidationErrorResponseModel(string errorMessage)
13 | : base(new List { errorMessage })
14 | {
15 | }
16 |
17 | ///
18 | /// The constructor with list of exception
19 | ///
20 | /// The List of exception
21 | public DefaultValidationErrorResponseModel(IEnumerable errorMessage)
22 | : base(errorMessage.ToList())
23 | {
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension/Infrastructure/Models/ValidationResultModel.cs:
--------------------------------------------------------------------------------
1 | using TechBuddy.Extension.Validation.Infrastructure.Models.ResponseModels;
2 |
3 | namespace TechBuddy.Extension.Validation.Infrastructure.Models;
4 |
5 | ///
6 | /// The default
7 | ///
8 | /// The generic type for
9 | public class ValidationResultModel : ValidationResultModel where T : new()
10 | {
11 | ///
12 | /// The instance of validated and casted model
13 | ///
14 | public T Model { get; set; }
15 | }
16 |
17 | ///
18 | /// The
19 | ///
20 | public class ValidationResultModel : BaseValidationErrorResponseModel
21 | {
22 | ///
23 | /// The parameterless constructor that sets Errors to null/>
24 | ///
25 | public ValidationResultModel()
26 | : this(null)
27 | {
28 |
29 | }
30 |
31 | ///
32 | /// The constructor that sets Errors/>
33 | ///
34 | /// The list of error
35 | public ValidationResultModel(List errors) : base(errors)
36 | {
37 | }
38 |
39 | ///
40 | /// Shows if the validation is successful
41 | ///
42 | public bool IsValid { get; set; }
43 | }
44 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension/ValidationExtension.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 |
7 |
8 | TechBuddy.Extension.Validation
9 | TechBuddy.Extension.Validation
10 |
11 |
12 |
13 |
14 |
15 | true
16 | true
17 | true
18 | TechBuddy.Extensions.Validation
19 | Validation; Validation Extensions; FluentValidation Extensions; FluentValidations;
20 |
21 | LICENSE.txt
22 | README.md
23 |
24 |
25 |
26 | TechBuddyTR Youtube
27 | Salih Cantekin, Community
28 | Provides extension methods for FluentValidation (IValidator) models on your WebAPI project
29 | logo.png
30 | TechBuddyTR Youtube
31 | https://github.com/TechBuddyTR/TechBuddy.Extensions
32 | https://github.com/TechBuddyTR/TechBuddy.Extensions
33 | git
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | Always
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension/licenses/license.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 [Salih Cantekin]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/ValidationExtension/ValidationExtension/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TechBuddyTR/TechBuddy.Extensions/df08c47466a9a5cf5d5bd472865743f2a6c2b8ae/src/ValidationExtension/ValidationExtension/logo.png
--------------------------------------------------------------------------------