├── .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 | [![Build](https://github.com/TechBuddyTR/TechBuddy.Extensions/actions/workflows/rb.yml/badge.svg?branch=master)]() 2 | 3 | ### Repo Stat 4 | 5 | [![](https://img.shields.io/github/commit-activity/m/techbuddytr/TechBuddy.Extensions?style=for-the-badge)](https://github.com/TechBuddyTR/TechBuddy.Extensions/commits/dev) 6 | [![](https://img.shields.io/github/contributors/TechBuddyTR/TechBuddy.Extensions?style=for-the-badge)](https://github.com/TechBuddyTR/TechBuddy.Extensions/graphs/contributors) 7 | [![](https://img.shields.io/github/issues-pr/TechBuddyTR/TechBuddy.Extensions?style=for-the-badge)](https://github.com/TechBuddyTR/TechBuddy.Extensions/pulls) 8 | [![](https://img.shields.io/github/issues-pr-closed/TechBuddyTR/TechBuddy.Extensions?style=for-the-badge)](https://github.com/TechBuddyTR/TechBuddy.Extensions/pulls?q=is%3Apr+is%3Aclosed) 9 | [![](https://img.shields.io/github/issues-raw/TechBuddyTR/TechBuddy.Extensions?style=for-the-badge)](https://github.com/TechBuddyTR/TechBuddy.Extensions/issues) 10 | [![](https://img.shields.io/github/issues-closed/TechBuddyTR/TechBuddy.Extensions?style=for-the-badge)](https://github.com/TechBuddyTR/TechBuddy.Extensions/issues?q=is%3Aissue+is%3Aclosed) 11 | 12 | [![](https://img.shields.io/github/repo-size/TechBuddyTR/TechBuddy.Extensions?style=for-the-badge)]() 13 | [![](https://img.shields.io/github/languages/code-size/TechBuddyTR/TechBuddy.Extensions?style=for-the-badge)]() 14 | [![](https://img.shields.io/github/directory-file-count/TechBuddyTR/TechBuddy.Extensions?style=for-the-badge)]() 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://img.shields.io/nuget/v/TechBuddy.Extensions.AspNetCore.ApiVersioning?style=for-the-badge)](https://www.nuget.org/packages/TechBuddy.Extensions.AspNetCore.ApiVersioning) | [![](https://img.shields.io/nuget/dt/TechBuddy.Extensions.AspNetCore.ApiVersioning?style=for-the-badge)](https://www.nuget.org/packages/TechBuddy.Extensions.AspNetCore.ApiVersioning/) | 33 | | Validation | [![](https://img.shields.io/nuget/v/TechBuddy.Extensions.Validation?style=for-the-badge)](https://www.nuget.org/packages/TechBuddy.Extensions.Validation) | [![](https://img.shields.io/nuget/dt/TechBuddy.Extensions.Validation?style=for-the-badge)](https://www.nuget.org/packages/TechBuddy.Extensions.Validation/) | 34 | | ExceptionHandling | [![](https://img.shields.io/nuget/v/TechBuddy.Extensions.AspNetCore.ExceptionHandling?style=for-the-badge)](https://www.nuget.org/packages/TechBuddy.Extensions.AspNetCore.ExceptionHandling) | [![](https://img.shields.io/nuget/dt/TechBuddy.Extensions.AspNetCore.ExceptionHandling?style=for-the-badge)](https://www.nuget.org/packages/TechBuddy.Extensions.AspNetCore.ExceptionHandling/) | 35 | | OpenApi | [![](https://img.shields.io/nuget/v/TechBuddy.Extensions.OpenApi?style=for-the-badge)](https://www.nuget.org/packages/TechBuddy.Extensions.OpenApi) | [![](https://img.shields.io/nuget/dt/TechBuddy.Extensions.OpenApi?style=for-the-badge)](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://img.shields.io/nuget/v/TechBuddy.Extensions.AspNetCore.ApiVersioning?style=for-the-badge)](https://www.nuget.org/packages/TechBuddy.Extensions.AspNetCore.ApiVersioning) | [![](https://img.shields.io/nuget/dt/TechBuddy.Extensions.AspNetCore.ApiVersioning?style=for-the-badge)](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://img.shields.io/nuget/v/TechBuddy.Extensions.AspNetCore.ExceptionHandling?style=for-the-badge)](https://www.nuget.org/packages/TechBuddy.Extensions.AspNetCore.ExceptionHandling) | [![](https://img.shields.io/nuget/dt/TechBuddy.Extensions.AspNetCore.ExceptionHandling?style=for-the-badge)](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 --------------------------------------------------------------------------------