diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 4799d8e..1b7addf 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -1,79 +1,80 @@ name: Build, Test, and Deploy on: - push: - branches: - - master - paths-ignore: - - 'samples/**' + push: + branches: + - master + paths-ignore: + - 'samples/**' - pull_request: - branches: - - master + pull_request: + branches: + - master - workflow_dispatch: - inputs: - deployToFeedz: - description: 'Deploy the libraries to feedz.io' - required: false - default: 'false' - deployToNuGet: - description: 'Deploy the libraries to NuGet.org' - required: false - default: 'false' + workflow_dispatch: + inputs: + deployToFeedz: + description: 'Deploy the libraries to feedz.io' + required: false + default: 'false' + deployToNuGet: + description: 'Deploy the libraries to NuGet.org' + required: false + default: 'false' env: - BUILD_CONFIGURATION: Release - ACTIONS_ALLOW_UNSECURE_COMMANDS: true + BUILD_CONFIGURATION: Release + # ACTIONS_ALLOW_UNSECURE_COMMANDS: true jobs: - build-and-test: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - dotnet: ['6.0.x'] + build-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 - steps: - - uses: actions/checkout@v1 + - name: Setup .NET Core + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 6.x + 7.x + 8.x - - name: Setup .NET Core - uses: actions/setup-dotnet@v1 - with: - dotnet-version: ${{ matrix.dotnet }} + - name: Build + run: dotnet build -c ${{ env.BUILD_CONFIGURATION }} - - name: Build - run: dotnet build --configuration ${{ env.BUILD_CONFIGURATION }} + - name: Unit Test + run: dotnet test -c ${{ env.BUILD_CONFIGURATION }} - - name: Unit Test - if: github.event_name == 'impossible' - run: dotnet test --configuration ${{ env.BUILD_CONFIGURATION }} + deploy: + runs-on: ubuntu-latest + needs: build-and-test + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 - deploy: - runs-on: ubuntu-latest - needs: build-and-test - strategy: - matrix: - dotnet: ['6.0.x'] - steps: - - uses: actions/checkout@v1 + - name: Setup .NET Core + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 6.x + 7.x + 8.x - - name: Setup .NET Core - uses: actions/setup-dotnet@v1 - with: - dotnet-version: ${{ matrix.dotnet }} + - uses: dotnet/nbgv@master + with: + setAllVars: true - - uses: aarnott/nbgv@v0.3 - with: - setAllVars: true + - name: Pack + run: dotnet pack -c ${{ env.BUILD_CONFIGURATION }} - - name: Pack - run: dotnet pack --configuration ${{ env.BUILD_CONFIGURATION }} + - name: Push to feedz.io + run: dotnet nuget push **/*.nupkg -k ${{ secrets.FEEDZ_API_KEY }} -s https://f.feedz.io/forevolve/exception-mapper/nuget/index.json + if: ${{ github.event.inputs.deployToFeedz == 'true' || github.event_name == 'pull_request' }} - - name: Push to feedz.io - run: dotnet nuget push **/*.nupkg -k ${{ secrets.FEEDZ_API_KEY }} -s https://f.feedz.io/forevolve/exception-mapper/nuget/index.json - if: ${{ github.event.inputs.deployToFeedz == 'true' || github.event_name == 'pull_request' }} - - - name: Push to NuGet.org - run: dotnet nuget push **/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json - if: ${{ github.event.inputs.deployToNuGet == 'true' || github.event_name == 'push' }} + - name: Push to NuGet.org + run: dotnet nuget push **/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json + if: ${{ github.event.inputs.deployToNuGet == 'true' || github.event_name == 'push' }} diff --git a/.prettierrc b/.prettierrc index 3b2860b..94a5815 100644 --- a/.prettierrc +++ b/.prettierrc @@ -5,10 +5,13 @@ "singleQuote": true, "overrides": [ { - "files": ["*.yaml"], + "files": [ + "*.yaml", + "*.yml" + ], "options": { "tabWidth": 2 } } ] -} +} \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index 33b216f..2a1e7a6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,6 +6,8 @@ git true Latest + net6.0;net7.0;net8.0 + $(FETargetFrameworks) \ No newline at end of file diff --git a/ForEvolve.ExceptionMapper.sln b/ForEvolve.ExceptionMapper.sln index 4d4cf6b..72e128c 100644 --- a/ForEvolve.ExceptionMapper.sln +++ b/ForEvolve.ExceptionMapper.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29728.190 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34004.107 MinimumVisualStudioVersion = 15.0.26124.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C2DD6A0D-F994-42A4-BAFF-1FB972850229}" ProjectSection(SolutionItems) = preProject @@ -23,45 +23,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Build.props = Directory.Build.props EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.ExceptionMapper.CommonExceptions", "src\ForEvolve.ExceptionMapper.CommonExceptions\ForEvolve.ExceptionMapper.CommonExceptions.csproj", "{E3E588AC-0B8B-4007-8680-95DBBC33457A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.ExceptionMapper.CommonExceptions.Tests", "test\ForEvolve.ExceptionMapper.CommonExceptions.Tests\ForEvolve.ExceptionMapper.CommonExceptions.Tests.csproj", "{00B82E0C-D20C-480B-8F66-9E05CA21D983}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{B236C0BC-2FEF-4F68-8EE3-A6281924D00F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.ExceptionMapper.FluentMapper", "src\ForEvolve.ExceptionMapper.FluentMapper\ForEvolve.ExceptionMapper.FluentMapper.csproj", "{AEEE6909-2811-405C-AEF0-318DF182373F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.ExceptionMapper.FluentMapper.Tests", "test\ForEvolve.ExceptionMapper.FluentMapper.Tests\ForEvolve.ExceptionMapper.FluentMapper.Tests.csproj", "{DE7D40BC-C533-4FFB-B920-F918BD64A99B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.ExceptionMapper.Core", "src\ForEvolve.ExceptionMapper.Core\ForEvolve.ExceptionMapper.Core.csproj", "{7D0F6B4A-5938-46C1-997B-0B0F75C05A54}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.ExceptionMapper.Core.Tests", "test\ForEvolve.ExceptionMapper.Core.Tests\ForEvolve.ExceptionMapper.Core.Tests.csproj", "{4FB70CDD-C460-4818-ABF8-BDE8E0C491B6}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.ExceptionMapper.Mvc", "src\ForEvolve.ExceptionMapper.Mvc\ForEvolve.ExceptionMapper.Mvc.csproj", "{D476D658-0E65-471D-87C4-2206FDA54539}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.ExceptionMapper.Mvc.Tests", "test\ForEvolve.ExceptionMapper.Mvc.Tests\ForEvolve.ExceptionMapper.Mvc.Tests.csproj", "{E40C2D59-5041-4586-8F8E-98C6D093F7AB}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.ExceptionMapper.HttpMiddleware", "src\ForEvolve.ExceptionMapper.HttpMiddleware\ForEvolve.ExceptionMapper.HttpMiddleware.csproj", "{4801A0E9-CA6B-458F-B0A5-55472EFECF08}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.ExceptionMapper.HttpMiddleware.Tests", "test\ForEvolve.ExceptionMapper.HttpMiddleware.Tests\ForEvolve.ExceptionMapper.HttpMiddleware.Tests.csproj", "{36BF7049-1E94-4B03-9F2A-94EC76747A37}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.ExceptionMapper.Scrutor", "src\ForEvolve.ExceptionMapper.Scrutor\ForEvolve.ExceptionMapper.Scrutor.csproj", "{7DE932E7-AA2F-4D80-A616-36FC8233020E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.ExceptionMapper.Scrutor.Tests", "test\ForEvolve.ExceptionMapper.Scrutor.Tests\ForEvolve.ExceptionMapper.Scrutor.Tests.csproj", "{9789A9A9-9C27-4E6F-B333-B0B2488637C6}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi.HttpMiddleware", "samples\WebApi.HttpMiddleware\WebApi.HttpMiddleware.csproj", "{EDF69289-5307-47D2-BD53-C5E523B69F6E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi.Mvc", "samples\WebApi.Mvc\WebApi.Mvc.csproj", "{B4D05F27-FD28-4983-8247-27759B7DE77C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers", "src\ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers\ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.csproj", "{E29EF017-7E0B-4F20-A1D3-08FA22EA05AD}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests", "test\ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests\ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests.csproj", "{CED04BA2-6293-48D9-92D4-33D58CBB5B56}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi.Shared", "samples\WebApi.Shared\WebApi.Shared.csproj", "{A5051881-D65D-4FB7-9E9C-D10FF48E54E1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.ExceptionMapper.Serialization.Json", "src\ForEvolve.ExceptionMapper.Serialization.Json\ForEvolve.ExceptionMapper.Serialization.Json.csproj", "{FB8BAD2F-F09C-486F-9461-40ECCCB17A37}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.ExceptionMapper.Serialization.Json.Tests", "test\ForEvolve.ExceptionMapper.Serialization.Json.Tests\ForEvolve.ExceptionMapper.Serialization.Json.Tests.csproj", "{D0092F11-F801-4C17-AB91-53C060030266}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi.Minimal", "samples\WebApi.Minimal\WebApi.Minimal.csproj", "{543A8584-25BC-43ED-B57D-CBFA6B59AA8C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -97,150 +65,6 @@ Global {241913CB-0426-4709-A3CF-00665C400DE5}.Release|x64.Build.0 = Release|Any CPU {241913CB-0426-4709-A3CF-00665C400DE5}.Release|x86.ActiveCfg = Release|Any CPU {241913CB-0426-4709-A3CF-00665C400DE5}.Release|x86.Build.0 = Release|Any CPU - {E3E588AC-0B8B-4007-8680-95DBBC33457A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E3E588AC-0B8B-4007-8680-95DBBC33457A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E3E588AC-0B8B-4007-8680-95DBBC33457A}.Debug|x64.ActiveCfg = Debug|Any CPU - {E3E588AC-0B8B-4007-8680-95DBBC33457A}.Debug|x64.Build.0 = Debug|Any CPU - {E3E588AC-0B8B-4007-8680-95DBBC33457A}.Debug|x86.ActiveCfg = Debug|Any CPU - {E3E588AC-0B8B-4007-8680-95DBBC33457A}.Debug|x86.Build.0 = Debug|Any CPU - {E3E588AC-0B8B-4007-8680-95DBBC33457A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E3E588AC-0B8B-4007-8680-95DBBC33457A}.Release|Any CPU.Build.0 = Release|Any CPU - {E3E588AC-0B8B-4007-8680-95DBBC33457A}.Release|x64.ActiveCfg = Release|Any CPU - {E3E588AC-0B8B-4007-8680-95DBBC33457A}.Release|x64.Build.0 = Release|Any CPU - {E3E588AC-0B8B-4007-8680-95DBBC33457A}.Release|x86.ActiveCfg = Release|Any CPU - {E3E588AC-0B8B-4007-8680-95DBBC33457A}.Release|x86.Build.0 = Release|Any CPU - {00B82E0C-D20C-480B-8F66-9E05CA21D983}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {00B82E0C-D20C-480B-8F66-9E05CA21D983}.Debug|Any CPU.Build.0 = Debug|Any CPU - {00B82E0C-D20C-480B-8F66-9E05CA21D983}.Debug|x64.ActiveCfg = Debug|Any CPU - {00B82E0C-D20C-480B-8F66-9E05CA21D983}.Debug|x64.Build.0 = Debug|Any CPU - {00B82E0C-D20C-480B-8F66-9E05CA21D983}.Debug|x86.ActiveCfg = Debug|Any CPU - {00B82E0C-D20C-480B-8F66-9E05CA21D983}.Debug|x86.Build.0 = Debug|Any CPU - {00B82E0C-D20C-480B-8F66-9E05CA21D983}.Release|Any CPU.ActiveCfg = Release|Any CPU - {00B82E0C-D20C-480B-8F66-9E05CA21D983}.Release|Any CPU.Build.0 = Release|Any CPU - {00B82E0C-D20C-480B-8F66-9E05CA21D983}.Release|x64.ActiveCfg = Release|Any CPU - {00B82E0C-D20C-480B-8F66-9E05CA21D983}.Release|x64.Build.0 = Release|Any CPU - {00B82E0C-D20C-480B-8F66-9E05CA21D983}.Release|x86.ActiveCfg = Release|Any CPU - {00B82E0C-D20C-480B-8F66-9E05CA21D983}.Release|x86.Build.0 = Release|Any CPU - {AEEE6909-2811-405C-AEF0-318DF182373F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AEEE6909-2811-405C-AEF0-318DF182373F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AEEE6909-2811-405C-AEF0-318DF182373F}.Debug|x64.ActiveCfg = Debug|Any CPU - {AEEE6909-2811-405C-AEF0-318DF182373F}.Debug|x64.Build.0 = Debug|Any CPU - {AEEE6909-2811-405C-AEF0-318DF182373F}.Debug|x86.ActiveCfg = Debug|Any CPU - {AEEE6909-2811-405C-AEF0-318DF182373F}.Debug|x86.Build.0 = Debug|Any CPU - {AEEE6909-2811-405C-AEF0-318DF182373F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AEEE6909-2811-405C-AEF0-318DF182373F}.Release|Any CPU.Build.0 = Release|Any CPU - {AEEE6909-2811-405C-AEF0-318DF182373F}.Release|x64.ActiveCfg = Release|Any CPU - {AEEE6909-2811-405C-AEF0-318DF182373F}.Release|x64.Build.0 = Release|Any CPU - {AEEE6909-2811-405C-AEF0-318DF182373F}.Release|x86.ActiveCfg = Release|Any CPU - {AEEE6909-2811-405C-AEF0-318DF182373F}.Release|x86.Build.0 = Release|Any CPU - {DE7D40BC-C533-4FFB-B920-F918BD64A99B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DE7D40BC-C533-4FFB-B920-F918BD64A99B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DE7D40BC-C533-4FFB-B920-F918BD64A99B}.Debug|x64.ActiveCfg = Debug|Any CPU - {DE7D40BC-C533-4FFB-B920-F918BD64A99B}.Debug|x64.Build.0 = Debug|Any CPU - {DE7D40BC-C533-4FFB-B920-F918BD64A99B}.Debug|x86.ActiveCfg = Debug|Any CPU - {DE7D40BC-C533-4FFB-B920-F918BD64A99B}.Debug|x86.Build.0 = Debug|Any CPU - {DE7D40BC-C533-4FFB-B920-F918BD64A99B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DE7D40BC-C533-4FFB-B920-F918BD64A99B}.Release|Any CPU.Build.0 = Release|Any CPU - {DE7D40BC-C533-4FFB-B920-F918BD64A99B}.Release|x64.ActiveCfg = Release|Any CPU - {DE7D40BC-C533-4FFB-B920-F918BD64A99B}.Release|x64.Build.0 = Release|Any CPU - {DE7D40BC-C533-4FFB-B920-F918BD64A99B}.Release|x86.ActiveCfg = Release|Any CPU - {DE7D40BC-C533-4FFB-B920-F918BD64A99B}.Release|x86.Build.0 = Release|Any CPU - {7D0F6B4A-5938-46C1-997B-0B0F75C05A54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7D0F6B4A-5938-46C1-997B-0B0F75C05A54}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7D0F6B4A-5938-46C1-997B-0B0F75C05A54}.Debug|x64.ActiveCfg = Debug|Any CPU - {7D0F6B4A-5938-46C1-997B-0B0F75C05A54}.Debug|x64.Build.0 = Debug|Any CPU - {7D0F6B4A-5938-46C1-997B-0B0F75C05A54}.Debug|x86.ActiveCfg = Debug|Any CPU - {7D0F6B4A-5938-46C1-997B-0B0F75C05A54}.Debug|x86.Build.0 = Debug|Any CPU - {7D0F6B4A-5938-46C1-997B-0B0F75C05A54}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7D0F6B4A-5938-46C1-997B-0B0F75C05A54}.Release|Any CPU.Build.0 = Release|Any CPU - {7D0F6B4A-5938-46C1-997B-0B0F75C05A54}.Release|x64.ActiveCfg = Release|Any CPU - {7D0F6B4A-5938-46C1-997B-0B0F75C05A54}.Release|x64.Build.0 = Release|Any CPU - {7D0F6B4A-5938-46C1-997B-0B0F75C05A54}.Release|x86.ActiveCfg = Release|Any CPU - {7D0F6B4A-5938-46C1-997B-0B0F75C05A54}.Release|x86.Build.0 = Release|Any CPU - {4FB70CDD-C460-4818-ABF8-BDE8E0C491B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4FB70CDD-C460-4818-ABF8-BDE8E0C491B6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4FB70CDD-C460-4818-ABF8-BDE8E0C491B6}.Debug|x64.ActiveCfg = Debug|Any CPU - {4FB70CDD-C460-4818-ABF8-BDE8E0C491B6}.Debug|x64.Build.0 = Debug|Any CPU - {4FB70CDD-C460-4818-ABF8-BDE8E0C491B6}.Debug|x86.ActiveCfg = Debug|Any CPU - {4FB70CDD-C460-4818-ABF8-BDE8E0C491B6}.Debug|x86.Build.0 = Debug|Any CPU - {4FB70CDD-C460-4818-ABF8-BDE8E0C491B6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4FB70CDD-C460-4818-ABF8-BDE8E0C491B6}.Release|Any CPU.Build.0 = Release|Any CPU - {4FB70CDD-C460-4818-ABF8-BDE8E0C491B6}.Release|x64.ActiveCfg = Release|Any CPU - {4FB70CDD-C460-4818-ABF8-BDE8E0C491B6}.Release|x64.Build.0 = Release|Any CPU - {4FB70CDD-C460-4818-ABF8-BDE8E0C491B6}.Release|x86.ActiveCfg = Release|Any CPU - {4FB70CDD-C460-4818-ABF8-BDE8E0C491B6}.Release|x86.Build.0 = Release|Any CPU - {D476D658-0E65-471D-87C4-2206FDA54539}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D476D658-0E65-471D-87C4-2206FDA54539}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D476D658-0E65-471D-87C4-2206FDA54539}.Debug|x64.ActiveCfg = Debug|Any CPU - {D476D658-0E65-471D-87C4-2206FDA54539}.Debug|x64.Build.0 = Debug|Any CPU - {D476D658-0E65-471D-87C4-2206FDA54539}.Debug|x86.ActiveCfg = Debug|Any CPU - {D476D658-0E65-471D-87C4-2206FDA54539}.Debug|x86.Build.0 = Debug|Any CPU - {D476D658-0E65-471D-87C4-2206FDA54539}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D476D658-0E65-471D-87C4-2206FDA54539}.Release|Any CPU.Build.0 = Release|Any CPU - {D476D658-0E65-471D-87C4-2206FDA54539}.Release|x64.ActiveCfg = Release|Any CPU - {D476D658-0E65-471D-87C4-2206FDA54539}.Release|x64.Build.0 = Release|Any CPU - {D476D658-0E65-471D-87C4-2206FDA54539}.Release|x86.ActiveCfg = Release|Any CPU - {D476D658-0E65-471D-87C4-2206FDA54539}.Release|x86.Build.0 = Release|Any CPU - {E40C2D59-5041-4586-8F8E-98C6D093F7AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E40C2D59-5041-4586-8F8E-98C6D093F7AB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E40C2D59-5041-4586-8F8E-98C6D093F7AB}.Debug|x64.ActiveCfg = Debug|Any CPU - {E40C2D59-5041-4586-8F8E-98C6D093F7AB}.Debug|x64.Build.0 = Debug|Any CPU - {E40C2D59-5041-4586-8F8E-98C6D093F7AB}.Debug|x86.ActiveCfg = Debug|Any CPU - {E40C2D59-5041-4586-8F8E-98C6D093F7AB}.Debug|x86.Build.0 = Debug|Any CPU - {E40C2D59-5041-4586-8F8E-98C6D093F7AB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E40C2D59-5041-4586-8F8E-98C6D093F7AB}.Release|Any CPU.Build.0 = Release|Any CPU - {E40C2D59-5041-4586-8F8E-98C6D093F7AB}.Release|x64.ActiveCfg = Release|Any CPU - {E40C2D59-5041-4586-8F8E-98C6D093F7AB}.Release|x64.Build.0 = Release|Any CPU - {E40C2D59-5041-4586-8F8E-98C6D093F7AB}.Release|x86.ActiveCfg = Release|Any CPU - {E40C2D59-5041-4586-8F8E-98C6D093F7AB}.Release|x86.Build.0 = Release|Any CPU - {4801A0E9-CA6B-458F-B0A5-55472EFECF08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4801A0E9-CA6B-458F-B0A5-55472EFECF08}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4801A0E9-CA6B-458F-B0A5-55472EFECF08}.Debug|x64.ActiveCfg = Debug|Any CPU - {4801A0E9-CA6B-458F-B0A5-55472EFECF08}.Debug|x64.Build.0 = Debug|Any CPU - {4801A0E9-CA6B-458F-B0A5-55472EFECF08}.Debug|x86.ActiveCfg = Debug|Any CPU - {4801A0E9-CA6B-458F-B0A5-55472EFECF08}.Debug|x86.Build.0 = Debug|Any CPU - {4801A0E9-CA6B-458F-B0A5-55472EFECF08}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4801A0E9-CA6B-458F-B0A5-55472EFECF08}.Release|Any CPU.Build.0 = Release|Any CPU - {4801A0E9-CA6B-458F-B0A5-55472EFECF08}.Release|x64.ActiveCfg = Release|Any CPU - {4801A0E9-CA6B-458F-B0A5-55472EFECF08}.Release|x64.Build.0 = Release|Any CPU - {4801A0E9-CA6B-458F-B0A5-55472EFECF08}.Release|x86.ActiveCfg = Release|Any CPU - {4801A0E9-CA6B-458F-B0A5-55472EFECF08}.Release|x86.Build.0 = Release|Any CPU - {36BF7049-1E94-4B03-9F2A-94EC76747A37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {36BF7049-1E94-4B03-9F2A-94EC76747A37}.Debug|Any CPU.Build.0 = Debug|Any CPU - {36BF7049-1E94-4B03-9F2A-94EC76747A37}.Debug|x64.ActiveCfg = Debug|Any CPU - {36BF7049-1E94-4B03-9F2A-94EC76747A37}.Debug|x64.Build.0 = Debug|Any CPU - {36BF7049-1E94-4B03-9F2A-94EC76747A37}.Debug|x86.ActiveCfg = Debug|Any CPU - {36BF7049-1E94-4B03-9F2A-94EC76747A37}.Debug|x86.Build.0 = Debug|Any CPU - {36BF7049-1E94-4B03-9F2A-94EC76747A37}.Release|Any CPU.ActiveCfg = Release|Any CPU - {36BF7049-1E94-4B03-9F2A-94EC76747A37}.Release|Any CPU.Build.0 = Release|Any CPU - {36BF7049-1E94-4B03-9F2A-94EC76747A37}.Release|x64.ActiveCfg = Release|Any CPU - {36BF7049-1E94-4B03-9F2A-94EC76747A37}.Release|x64.Build.0 = Release|Any CPU - {36BF7049-1E94-4B03-9F2A-94EC76747A37}.Release|x86.ActiveCfg = Release|Any CPU - {36BF7049-1E94-4B03-9F2A-94EC76747A37}.Release|x86.Build.0 = Release|Any CPU - {7DE932E7-AA2F-4D80-A616-36FC8233020E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7DE932E7-AA2F-4D80-A616-36FC8233020E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7DE932E7-AA2F-4D80-A616-36FC8233020E}.Debug|x64.ActiveCfg = Debug|Any CPU - {7DE932E7-AA2F-4D80-A616-36FC8233020E}.Debug|x64.Build.0 = Debug|Any CPU - {7DE932E7-AA2F-4D80-A616-36FC8233020E}.Debug|x86.ActiveCfg = Debug|Any CPU - {7DE932E7-AA2F-4D80-A616-36FC8233020E}.Debug|x86.Build.0 = Debug|Any CPU - {7DE932E7-AA2F-4D80-A616-36FC8233020E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7DE932E7-AA2F-4D80-A616-36FC8233020E}.Release|Any CPU.Build.0 = Release|Any CPU - {7DE932E7-AA2F-4D80-A616-36FC8233020E}.Release|x64.ActiveCfg = Release|Any CPU - {7DE932E7-AA2F-4D80-A616-36FC8233020E}.Release|x64.Build.0 = Release|Any CPU - {7DE932E7-AA2F-4D80-A616-36FC8233020E}.Release|x86.ActiveCfg = Release|Any CPU - {7DE932E7-AA2F-4D80-A616-36FC8233020E}.Release|x86.Build.0 = Release|Any CPU - {9789A9A9-9C27-4E6F-B333-B0B2488637C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9789A9A9-9C27-4E6F-B333-B0B2488637C6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9789A9A9-9C27-4E6F-B333-B0B2488637C6}.Debug|x64.ActiveCfg = Debug|Any CPU - {9789A9A9-9C27-4E6F-B333-B0B2488637C6}.Debug|x64.Build.0 = Debug|Any CPU - {9789A9A9-9C27-4E6F-B333-B0B2488637C6}.Debug|x86.ActiveCfg = Debug|Any CPU - {9789A9A9-9C27-4E6F-B333-B0B2488637C6}.Debug|x86.Build.0 = Debug|Any CPU - {9789A9A9-9C27-4E6F-B333-B0B2488637C6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9789A9A9-9C27-4E6F-B333-B0B2488637C6}.Release|Any CPU.Build.0 = Release|Any CPU - {9789A9A9-9C27-4E6F-B333-B0B2488637C6}.Release|x64.ActiveCfg = Release|Any CPU - {9789A9A9-9C27-4E6F-B333-B0B2488637C6}.Release|x64.Build.0 = Release|Any CPU - {9789A9A9-9C27-4E6F-B333-B0B2488637C6}.Release|x86.ActiveCfg = Release|Any CPU - {9789A9A9-9C27-4E6F-B333-B0B2488637C6}.Release|x86.Build.0 = Release|Any CPU {EDF69289-5307-47D2-BD53-C5E523B69F6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EDF69289-5307-47D2-BD53-C5E523B69F6E}.Debug|Any CPU.Build.0 = Debug|Any CPU {EDF69289-5307-47D2-BD53-C5E523B69F6E}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -253,42 +77,6 @@ Global {EDF69289-5307-47D2-BD53-C5E523B69F6E}.Release|x64.Build.0 = Release|Any CPU {EDF69289-5307-47D2-BD53-C5E523B69F6E}.Release|x86.ActiveCfg = Release|Any CPU {EDF69289-5307-47D2-BD53-C5E523B69F6E}.Release|x86.Build.0 = Release|Any CPU - {B4D05F27-FD28-4983-8247-27759B7DE77C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B4D05F27-FD28-4983-8247-27759B7DE77C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B4D05F27-FD28-4983-8247-27759B7DE77C}.Debug|x64.ActiveCfg = Debug|Any CPU - {B4D05F27-FD28-4983-8247-27759B7DE77C}.Debug|x64.Build.0 = Debug|Any CPU - {B4D05F27-FD28-4983-8247-27759B7DE77C}.Debug|x86.ActiveCfg = Debug|Any CPU - {B4D05F27-FD28-4983-8247-27759B7DE77C}.Debug|x86.Build.0 = Debug|Any CPU - {B4D05F27-FD28-4983-8247-27759B7DE77C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B4D05F27-FD28-4983-8247-27759B7DE77C}.Release|Any CPU.Build.0 = Release|Any CPU - {B4D05F27-FD28-4983-8247-27759B7DE77C}.Release|x64.ActiveCfg = Release|Any CPU - {B4D05F27-FD28-4983-8247-27759B7DE77C}.Release|x64.Build.0 = Release|Any CPU - {B4D05F27-FD28-4983-8247-27759B7DE77C}.Release|x86.ActiveCfg = Release|Any CPU - {B4D05F27-FD28-4983-8247-27759B7DE77C}.Release|x86.Build.0 = Release|Any CPU - {E29EF017-7E0B-4F20-A1D3-08FA22EA05AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E29EF017-7E0B-4F20-A1D3-08FA22EA05AD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E29EF017-7E0B-4F20-A1D3-08FA22EA05AD}.Debug|x64.ActiveCfg = Debug|Any CPU - {E29EF017-7E0B-4F20-A1D3-08FA22EA05AD}.Debug|x64.Build.0 = Debug|Any CPU - {E29EF017-7E0B-4F20-A1D3-08FA22EA05AD}.Debug|x86.ActiveCfg = Debug|Any CPU - {E29EF017-7E0B-4F20-A1D3-08FA22EA05AD}.Debug|x86.Build.0 = Debug|Any CPU - {E29EF017-7E0B-4F20-A1D3-08FA22EA05AD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E29EF017-7E0B-4F20-A1D3-08FA22EA05AD}.Release|Any CPU.Build.0 = Release|Any CPU - {E29EF017-7E0B-4F20-A1D3-08FA22EA05AD}.Release|x64.ActiveCfg = Release|Any CPU - {E29EF017-7E0B-4F20-A1D3-08FA22EA05AD}.Release|x64.Build.0 = Release|Any CPU - {E29EF017-7E0B-4F20-A1D3-08FA22EA05AD}.Release|x86.ActiveCfg = Release|Any CPU - {E29EF017-7E0B-4F20-A1D3-08FA22EA05AD}.Release|x86.Build.0 = Release|Any CPU - {CED04BA2-6293-48D9-92D4-33D58CBB5B56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CED04BA2-6293-48D9-92D4-33D58CBB5B56}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CED04BA2-6293-48D9-92D4-33D58CBB5B56}.Debug|x64.ActiveCfg = Debug|Any CPU - {CED04BA2-6293-48D9-92D4-33D58CBB5B56}.Debug|x64.Build.0 = Debug|Any CPU - {CED04BA2-6293-48D9-92D4-33D58CBB5B56}.Debug|x86.ActiveCfg = Debug|Any CPU - {CED04BA2-6293-48D9-92D4-33D58CBB5B56}.Debug|x86.Build.0 = Debug|Any CPU - {CED04BA2-6293-48D9-92D4-33D58CBB5B56}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CED04BA2-6293-48D9-92D4-33D58CBB5B56}.Release|Any CPU.Build.0 = Release|Any CPU - {CED04BA2-6293-48D9-92D4-33D58CBB5B56}.Release|x64.ActiveCfg = Release|Any CPU - {CED04BA2-6293-48D9-92D4-33D58CBB5B56}.Release|x64.Build.0 = Release|Any CPU - {CED04BA2-6293-48D9-92D4-33D58CBB5B56}.Release|x86.ActiveCfg = Release|Any CPU - {CED04BA2-6293-48D9-92D4-33D58CBB5B56}.Release|x86.Build.0 = Release|Any CPU {A5051881-D65D-4FB7-9E9C-D10FF48E54E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A5051881-D65D-4FB7-9E9C-D10FF48E54E1}.Debug|Any CPU.Build.0 = Debug|Any CPU {A5051881-D65D-4FB7-9E9C-D10FF48E54E1}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -301,30 +89,18 @@ Global {A5051881-D65D-4FB7-9E9C-D10FF48E54E1}.Release|x64.Build.0 = Release|Any CPU {A5051881-D65D-4FB7-9E9C-D10FF48E54E1}.Release|x86.ActiveCfg = Release|Any CPU {A5051881-D65D-4FB7-9E9C-D10FF48E54E1}.Release|x86.Build.0 = Release|Any CPU - {FB8BAD2F-F09C-486F-9461-40ECCCB17A37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FB8BAD2F-F09C-486F-9461-40ECCCB17A37}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FB8BAD2F-F09C-486F-9461-40ECCCB17A37}.Debug|x64.ActiveCfg = Debug|Any CPU - {FB8BAD2F-F09C-486F-9461-40ECCCB17A37}.Debug|x64.Build.0 = Debug|Any CPU - {FB8BAD2F-F09C-486F-9461-40ECCCB17A37}.Debug|x86.ActiveCfg = Debug|Any CPU - {FB8BAD2F-F09C-486F-9461-40ECCCB17A37}.Debug|x86.Build.0 = Debug|Any CPU - {FB8BAD2F-F09C-486F-9461-40ECCCB17A37}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FB8BAD2F-F09C-486F-9461-40ECCCB17A37}.Release|Any CPU.Build.0 = Release|Any CPU - {FB8BAD2F-F09C-486F-9461-40ECCCB17A37}.Release|x64.ActiveCfg = Release|Any CPU - {FB8BAD2F-F09C-486F-9461-40ECCCB17A37}.Release|x64.Build.0 = Release|Any CPU - {FB8BAD2F-F09C-486F-9461-40ECCCB17A37}.Release|x86.ActiveCfg = Release|Any CPU - {FB8BAD2F-F09C-486F-9461-40ECCCB17A37}.Release|x86.Build.0 = Release|Any CPU - {D0092F11-F801-4C17-AB91-53C060030266}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D0092F11-F801-4C17-AB91-53C060030266}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D0092F11-F801-4C17-AB91-53C060030266}.Debug|x64.ActiveCfg = Debug|Any CPU - {D0092F11-F801-4C17-AB91-53C060030266}.Debug|x64.Build.0 = Debug|Any CPU - {D0092F11-F801-4C17-AB91-53C060030266}.Debug|x86.ActiveCfg = Debug|Any CPU - {D0092F11-F801-4C17-AB91-53C060030266}.Debug|x86.Build.0 = Debug|Any CPU - {D0092F11-F801-4C17-AB91-53C060030266}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D0092F11-F801-4C17-AB91-53C060030266}.Release|Any CPU.Build.0 = Release|Any CPU - {D0092F11-F801-4C17-AB91-53C060030266}.Release|x64.ActiveCfg = Release|Any CPU - {D0092F11-F801-4C17-AB91-53C060030266}.Release|x64.Build.0 = Release|Any CPU - {D0092F11-F801-4C17-AB91-53C060030266}.Release|x86.ActiveCfg = Release|Any CPU - {D0092F11-F801-4C17-AB91-53C060030266}.Release|x86.Build.0 = Release|Any CPU + {543A8584-25BC-43ED-B57D-CBFA6B59AA8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {543A8584-25BC-43ED-B57D-CBFA6B59AA8C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {543A8584-25BC-43ED-B57D-CBFA6B59AA8C}.Debug|x64.ActiveCfg = Debug|Any CPU + {543A8584-25BC-43ED-B57D-CBFA6B59AA8C}.Debug|x64.Build.0 = Debug|Any CPU + {543A8584-25BC-43ED-B57D-CBFA6B59AA8C}.Debug|x86.ActiveCfg = Debug|Any CPU + {543A8584-25BC-43ED-B57D-CBFA6B59AA8C}.Debug|x86.Build.0 = Debug|Any CPU + {543A8584-25BC-43ED-B57D-CBFA6B59AA8C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {543A8584-25BC-43ED-B57D-CBFA6B59AA8C}.Release|Any CPU.Build.0 = Release|Any CPU + {543A8584-25BC-43ED-B57D-CBFA6B59AA8C}.Release|x64.ActiveCfg = Release|Any CPU + {543A8584-25BC-43ED-B57D-CBFA6B59AA8C}.Release|x64.Build.0 = Release|Any CPU + {543A8584-25BC-43ED-B57D-CBFA6B59AA8C}.Release|x86.ActiveCfg = Release|Any CPU + {543A8584-25BC-43ED-B57D-CBFA6B59AA8C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -332,25 +108,9 @@ Global GlobalSection(NestedProjects) = preSolution {06C42E2C-A4DB-4CA8-870E-EA84727DF28C} = {C2DD6A0D-F994-42A4-BAFF-1FB972850229} {241913CB-0426-4709-A3CF-00665C400DE5} = {F5AE538B-E864-425E-959A-7ED49533482E} - {E3E588AC-0B8B-4007-8680-95DBBC33457A} = {C2DD6A0D-F994-42A4-BAFF-1FB972850229} - {00B82E0C-D20C-480B-8F66-9E05CA21D983} = {F5AE538B-E864-425E-959A-7ED49533482E} - {AEEE6909-2811-405C-AEF0-318DF182373F} = {C2DD6A0D-F994-42A4-BAFF-1FB972850229} - {DE7D40BC-C533-4FFB-B920-F918BD64A99B} = {F5AE538B-E864-425E-959A-7ED49533482E} - {7D0F6B4A-5938-46C1-997B-0B0F75C05A54} = {C2DD6A0D-F994-42A4-BAFF-1FB972850229} - {4FB70CDD-C460-4818-ABF8-BDE8E0C491B6} = {F5AE538B-E864-425E-959A-7ED49533482E} - {D476D658-0E65-471D-87C4-2206FDA54539} = {C2DD6A0D-F994-42A4-BAFF-1FB972850229} - {E40C2D59-5041-4586-8F8E-98C6D093F7AB} = {F5AE538B-E864-425E-959A-7ED49533482E} - {4801A0E9-CA6B-458F-B0A5-55472EFECF08} = {C2DD6A0D-F994-42A4-BAFF-1FB972850229} - {36BF7049-1E94-4B03-9F2A-94EC76747A37} = {F5AE538B-E864-425E-959A-7ED49533482E} - {7DE932E7-AA2F-4D80-A616-36FC8233020E} = {C2DD6A0D-F994-42A4-BAFF-1FB972850229} - {9789A9A9-9C27-4E6F-B333-B0B2488637C6} = {F5AE538B-E864-425E-959A-7ED49533482E} {EDF69289-5307-47D2-BD53-C5E523B69F6E} = {B236C0BC-2FEF-4F68-8EE3-A6281924D00F} - {B4D05F27-FD28-4983-8247-27759B7DE77C} = {B236C0BC-2FEF-4F68-8EE3-A6281924D00F} - {E29EF017-7E0B-4F20-A1D3-08FA22EA05AD} = {C2DD6A0D-F994-42A4-BAFF-1FB972850229} - {CED04BA2-6293-48D9-92D4-33D58CBB5B56} = {F5AE538B-E864-425E-959A-7ED49533482E} {A5051881-D65D-4FB7-9E9C-D10FF48E54E1} = {B236C0BC-2FEF-4F68-8EE3-A6281924D00F} - {FB8BAD2F-F09C-486F-9461-40ECCCB17A37} = {C2DD6A0D-F994-42A4-BAFF-1FB972850229} - {D0092F11-F801-4C17-AB91-53C060030266} = {F5AE538B-E864-425E-959A-7ED49533482E} + {543A8584-25BC-43ED-B57D-CBFA6B59AA8C} = {B236C0BC-2FEF-4F68-8EE3-A6281924D00F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4D008587-73AC-49F3-A485-00D80DA2B10D} diff --git a/README.md b/README.md index 9ba8fdc..4d561c4 100644 --- a/README.md +++ b/README.md @@ -1,84 +1,119 @@ -# ForEvolve.ExceptionMapper +# ExceptionMapper ![Build, Test, and Deploy](https://github.com/ForEvolve/ForEvolve.ExceptionMapper/workflows/Build,%20Test,%20and%20Deploy/badge.svg) +[![NuGet.org](https://img.shields.io/nuget/vpre/ForEvolve.ExceptionMapper)](https://www.nuget.org/packages/ForEvolve.ExceptionMapper/) +[![feedz.io](https://img.shields.io/badge/endpoint.svg?url=https%3A%2F%2Ff.feedz.io%2Fforevolve%2Fexception-mapper%2Fshield%2FForEvolve.ExceptionMapper%2Flatest)](https://f.feedz.io/forevolve/exception-mapper/packages/ForEvolve.ExceptionMapper/latest/download) -Asp.Net Core middleware that reacts to `Exception`. -You can map certain exception types to HTTP Status Code for example. -There are a few predefined exceptions and their respective handler, but you can do whatever you need with this. +A simple ASP.NET Core middleware that intercepts and reacts to `Exception`. +You can map specific exception types to HTTP Status Code, use predefined handlers, or create your own. + +You can throw an exception from anywhere in your codebase and ExceptionMapper will handle it according to your specifications. +This makes it a breeze to uniformize exception handling in a REST API. All of the handlers are iterated through, in order, so you can build a pipeline to handle exceptions where multiple handlers have a single responsibility. -For example, you could have handlers that respond to certain exception types, then one or more fallback handlers that react only if no previous handler handled the exception. Finally, there could be a serializer that convert handled exceptions to JSON, in the format of your choice, making your API linear between endpoints and exception types without much effort. +For example, you could have handlers that respond to certain exception types, then one or more fallback handlers that react only if no previous handler handled the exception. -> I plan on adding serialization handlers and Asp.Net Core MVC specific tweaks in a near future. +Finally, there is a serializer that convert handled exceptions to JSON, in the format of your choice, making your API linear between endpoints and exception types without much effort. The default serializer converts the errors to [Problem Details for HTTP APIs](https://datatracker.ietf.org/doc/html/rfc7807). ## Versioning -The packages follows _semantic versioning_. I use `Nerdbank.GitVersioning` to automatically version packages based on git commits/hashes. +The packages follows _semantic versioning_ and use `Nerdbank.GitVersioning` to automatically version packages based on git commits. ### Pre-released Prerelease packages are packaged code not yet merged to `master`. -The prerelease packages are hosted at [feedz.io](feedz.io), thanks to their "Open Source" subscription. +The prerelease CI builds are packaged and hosted at [feedz.io](feedz.io), thanks to their "Open Source" subscription. -### Released +## How to install -The release and some preview packages are published on [NuGet.org](https://www.nuget.org/). +Add a reference to the `ForEvolve.ExceptionMapper` NuGet package: -## How to install +```bash +dotnet add package ForEvolve.ExceptionMapper +``` -Load the `ForEvolve.ExceptionMapper` NuGet package or one or more of the following if you prefer loading only part of the _ExceptionMapper_. +_You can take a look at the `samples/WebApiSample` project for a working example._ -**List of packages** +## Getting started -| Name | NuGet.org | feedz.io | -| -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `dotnet add package ForEvolve.ExceptionMapper` | [![NuGet.org](https://img.shields.io/nuget/vpre/ForEvolve.ExceptionMapper)](https://www.nuget.org/packages/ForEvolve.ExceptionMapper/) | [![feedz.io](https://img.shields.io/badge/endpoint.svg?url=https%3A%2F%2Ff.feedz.io%2Fforevolve%2Fexception-mapper%2Fshield%2FForEvolve.ExceptionMapper%2Flatest)](https://f.feedz.io/forevolve/exception-mapper/packages/ForEvolve.ExceptionMapper/latest/download) | -| `dotnet add package ForEvolve.ExceptionMapper.CommonExceptions` | [![NuGet.org](https://img.shields.io/nuget/vpre/ForEvolve.ExceptionMapper.CommonExceptions)](https://www.nuget.org/packages/ForEvolve.ExceptionMapper.CommonExceptions/) | [![feedz.io](https://img.shields.io/badge/endpoint.svg?url=https%3A%2F%2Ff.feedz.io%2Fforevolve%2Fexception-mapper%2Fshield%2FForEvolve.ExceptionMapper.CommonExceptions%2Flatest)](https://f.feedz.io/forevolve/exception-mapper/packages/ForEvolve.ExceptionMapper.CommonExceptions/latest/download) | -| `dotnet add package ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers` | [![NuGet.org](https://img.shields.io/nuget/vpre/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers)](https://www.nuget.org/packages/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/) | [![feedz.io](https://img.shields.io/badge/endpoint.svg?url=https%3A%2F%2Ff.feedz.io%2Fforevolve%2Fexception-mapper%2Fshield%2FForEvolve.ExceptionMapper.CommonHttpExceptionHandlers%2Flatest)](https://f.feedz.io/forevolve/exception-mapper/packages/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/latest/download) | -| `dotnet add package ForEvolve.ExceptionMapper.Core` | [![NuGet.org](https://img.shields.io/nuget/vpre/ForEvolve.ExceptionMapper.Core)](https://www.nuget.org/packages/ForEvolve.ExceptionMapper.Core/) | [![feedz.io](https://img.shields.io/badge/endpoint.svg?url=https%3A%2F%2Ff.feedz.io%2Fforevolve%2Fexception-mapper%2Fshield%2FForEvolve.ExceptionMapper.Core%2Flatest)](https://f.feedz.io/forevolve/exception-mapper/packages/ForEvolve.ExceptionMapper.Core/latest/download) | -| `dotnet add package ForEvolve.ExceptionMapper.FluentMapper` | [![NuGet.org](https://img.shields.io/nuget/vpre/ForEvolve.ExceptionMapper.FluentMapper)](https://www.nuget.org/packages/ForEvolve.ExceptionMapper.FluentMapper/) | [![feedz.io](https://img.shields.io/badge/endpoint.svg?url=https%3A%2F%2Ff.feedz.io%2Fforevolve%2Fexception-mapper%2Fshield%2FForEvolve.ExceptionMapper.FluentMapper%2Flatest)](https://f.feedz.io/forevolve/exception-mapper/packages/ForEvolve.ExceptionMapper.FluentMapper/latest/download) | -| `dotnet add package ForEvolve.ExceptionMapper.HttpMiddleware` | [![NuGet.org](https://img.shields.io/nuget/vpre/ForEvolve.ExceptionMapper.HttpMiddleware)](https://www.nuget.org/packages/ForEvolve.ExceptionMapper.HttpMiddleware/) | [![feedz.io](https://img.shields.io/badge/endpoint.svg?url=https%3A%2F%2Ff.feedz.io%2Fforevolve%2Fexception-mapper%2Fshield%2FForEvolve.ExceptionMapper.HttpMiddleware%2Flatest)](https://f.feedz.io/forevolve/exception-mapper/packages/ForEvolve.ExceptionMapper.HttpMiddleware/latest/download) | -| `dotnet add package ForEvolve.ExceptionMapper.Scrutor` | [![NuGet.org](https://img.shields.io/nuget/vpre/ForEvolve.ExceptionMapper.Scrutor)](https://www.nuget.org/packages/ForEvolve.ExceptionMapper.Scrutor/) | [![feedz.io](https://img.shields.io/badge/endpoint.svg?url=https%3A%2F%2Ff.feedz.io%2Fforevolve%2Fexception-mapper%2Fshield%2FForEvolve.ExceptionMapper.Scrutor%2Flatest)](https://f.feedz.io/forevolve/exception-mapper/packages/ForEvolve.ExceptionMapper.Scrutor/latest/download) | -| `dotnet add package ForEvolve.ExceptionMapper.Serialization.Json` | [![NuGet.org](https://img.shields.io/nuget/vpre/ForEvolve.ExceptionMapper.Serialization.Json)](https://www.nuget.org/packages/ForEvolve.ExceptionMapper.Serialization.Json/) | [![feedz.io](https://img.shields.io/badge/endpoint.svg?url=https%3A%2F%2Ff.feedz.io%2Fforevolve%2Fexception-mapper%2Fshield%2FForEvolve.ExceptionMapper.Serialization.Json%2Flatest)](https://f.feedz.io/forevolve/exception-mapper/packages/ForEvolve.ExceptionMapper.Serialization.Json/latest/download) | +You must register the services, and optionally configure/register handlers, and use the middleware that catches exceptions (and that handles the logic). -# ForEvolve.ExceptionMapper +**Program.cs** -This package references the other packages. +```csharp +// Add the dependencies to the container +builder.AddExceptionMapper(); -# ForEvolve.ExceptionMapper.Core +// Register the middleware +app.UseExceptionMapper(); +``` -This package contains the core components like the interfaces, the middleware, etc. -You can load only this package to create your own exceptions and handlers. +**Startup.cs** -_You can take a look at the `samples/WebApiSample` project for a working example._ +```csharp +public class Startup +{ + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + public IConfiguration Configuration { get; } -## Getting started + public void ConfigureServices(IServiceCollection services) + { + // ... + services.AddExceptionMapper(Configuration); + // ... + } + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + //... + app.UseExceptionMapper(); // Register the middleware + //... + } +} +``` -You must register the services, and optionally configure/register handlers, and use the middleware that catches exceptions (and that handles the logic). +## Extending the existing exception + +An easy way to manage your custom exceptions is to inherit from the one that are already mapped. +For example, you could create and throw the following `DroidNotFoundException` and ExceptionMapper will associate it with a 404 Not Found status code because it inherits from `NotFoundException`: ```csharp -public void ConfigureServices(IServiceCollection services) +public class DroidNotFoundException : NotFoundException { - services.AddExceptionMapper(builder => builder - // Configure your pipeline here - ); + public DroidNotFoundException() + : base("These aren't the droids we're looking for.") + { + } } -public void Configure(IApplicationBuilder app, IWebHostEnvironment env) +``` + +## Mapping Exception types to status code + +If you do not want or can't inherit the provided exceptions, you can map any Exception types to specific status code, like this: + +```csharp +builder.AddExceptionMapper(builder => +{ + builder.Map().ToStatusCode(StatusCodes.Status418ImATeapot); +}); + +public class ImATeapotException : Exception { - //... - app.UseExceptionMapper(); // Register the middleware - //... } ``` -## Custom exception handler (simple) +## Creating a custom exception handler -If you want to create a custom exception handler that changes the status code and you don't want to use the `FluentMapper`. Maybe you simply like types or you want to scan one or more assembly to load mappers dynamically; no matter why, you can inherit from `ExceptionHandler`. +If the previous options are not enough to handle your use case, you can implement a custom exception handler. +To do so, you can inherit from the `ExceptionHandler` or implement the `IExceptionHandler` interface. +Then you must register it using the `AddExceptionHandler` extension method. Let's start by creating an exception: ```csharp -public class MyForbiddenException : Exception { } +public class MyForbiddenException : Exception { /* Omitted implementation */ } ``` Then create the handler: @@ -90,217 +125,194 @@ public class MyForbiddenExceptionHandler : ExceptionHandler builder - .AddExceptionHandler() -); +builder.AddExceptionMapper(builder => +{ + builder.AddExceptionHandler(); +}); ``` -If you want a bit more control, you can override `Task ExecuteCoreAsync(ExceptionHandlingContext context)` method and add custom code. +## Updating the type URI -## Custom exception handler (complex) - -If you want to create a custom exception handler, implement `IExceptionHandler`, then register that handler using the `AddExceptionHandler()` extension method. - -In our case, let's create an exception: +You can customize the `type` property of the problem details object by configuring the `ApiBehaviorOptions` object, like this: ```csharp -public class ImATeapotException : Exception { } +builder.Services + .Configure(options => + { + options.ClientErrorMapping.Add(StatusCodes.Status409Conflict, new ClientErrorData + { + Link = "https://localhost:8828/Status409Conflict", // This is taken into account because the middleware do not set any link by default. + Title = "This will not be displayed." // Not taken into account because the middleware sets the title to the Exception.Message value. + }); + }) +; ``` -Then the handler: +If using MVC the mappings should be registered so you may want to modify the data instead: ```csharp -public class ImATeapotExceptionHandler : IExceptionHandler -{ - public int Order => HandlerOrder.DefaultOrder; - - public async Task ExecuteAsync(ExceptionHandlingContext context) +builder.Services + .Configure(options => { - var response = context.HttpContext.Response; - response.StatusCode = StatusCodes.Status418ImATeapot; - response.ContentType = "text/html"; - context.Result = new ExceptionHandledResult(context.Error); - await response.WriteAsync("
");
-        await response.WriteAsync(@"             ;,'
-    _o_    ;:;'
-,-.'---`.__ ;
-((j`=====',-'
-`-\     /
-`-=-'     hjw
-Source: https://www.asciiart.eu/food-and-drinks/coffee-and-tea");
-        await response.WriteAsync("
"); - } - - public Task KnowHowToHandleAsync(Exception exception) - { - return Task.FromResult(exception is ImATeapotException); - } -} + options.ClientErrorMapping[StatusCodes.Status409Conflict].Link = "https://localhost:8828/Status409Conflict"; + }) +; ``` -It is very important to set the `context.Result.ExceptionHandled` to `true`. The easiest way to achieve that is to create an instance of the `ExceptionHandledResult` class. +# Common Exceptions -Once that's done, we can register it: +ExceptionMapper implements different common exceptions and their handlers, like the following maps (namespace `ForEvolve.ExceptionMapper`): ```csharp -services.AddExceptionMapper(builder => builder - .AddExceptionHandler() -); +// Common client exceptions +.Map().ToStatusCode(StatusCodes.Status400BadRequest) +.Map().ToStatusCode(StatusCodes.Status409Conflict) +.Map().ToStatusCode(StatusCodes.Status403Forbidden) +.Map().ToStatusCode(StatusCodes.Status410Gone) +.Map().ToStatusCode(StatusCodes.Status404NotFound) +.Map().ToStatusCode(StatusCodes.Status404NotFound) +.Map().ToStatusCode(StatusCodes.Status401Unauthorized) + +// .NET exceptions +.Map().ToStatusCode(StatusCodes.Status400BadRequest) +.Map().ToStatusCode(StatusCodes.Status501NotImplemented) + +// Common server exceptions +.Map().ToStatusCode(StatusCodes.Status504GatewayTimeout) +.Map().ToStatusCode(StatusCodes.Status500InternalServerError) +.Map().ToStatusCode(StatusCodes.Status503ServiceUnavailable) ``` -## Assembly Scanning - -You can also scan assemblies for `IExceptionHandler` using `IExceptionMappingBuilder` extensions. -For example, you could scan the `Startup` assembly for handlers: +# Fallback handler -```csharp -services.AddExceptionMapper(builder => builder - .ScanHandlersFromAssemblyOf() -); -``` +ExceptionMapper also comes with a fallback handler that convert unhandled exceptions to `500 InternalServerError`. This is an opt-out feature, configured by the `FallbackExceptionHandlerOptions`. -This feature uses Scrutor under the hood and exposes part of it to simplify things, so you could use it and use the `ScanHandlersFrom()` extensions that exposes the Scrutor `ITypeSourceSelector`, like: +You can also configure the `FallbackExceptionHandlerOptions` like the following or under the `ExceptionMapper:FallbackExceptionHandler` key in your settings: ```csharp -services.AddExceptionMapper(builder => builder - .ScanHandlersFrom(typeSourceSelector => typeSourceSelector.FromCallingAssembly()) -); +services.Configure(options => +{ + options.Strategy = FallbackStrategy.Handle; +}); ``` -# ForEvolve.ExceptionMapper.CommonExceptions - -This package implements different common exceptions and their handlers, like: +# Serialization (Json) -- `404 NotFound` (`ForEvolve.ExceptionMapper.NotFoundException`) -- `409 Conflict` (`ForEvolve.ExceptionMapper.ConflictException`) -- `500 InternalServerError` (`ForEvolve.ExceptionMapper.InternalServerErrorException`) -- `501 NotImplemented` (`System.NotImplementedException`) +ExceptionMapper provides a default implementation of the `IExceptionSerializer` interface that serializes exceptions as `ProblemDetails`. +When targetting .NET 7+, ExceptionMapper uses the `IProblemDetailsService` interface from ASP.NET Core. -It also comes with a fallback handler that convert unhandled exceptions to `500 InternalServerError`. This is an opt-in feature, configured by the `FallbackExceptionHandlerOptions`. That handler can be useful, but be careful of what you share about your exceptions in production, this could help a malicious user acquire information about your server. - -To use the prebuilt handlers, you have to: +If you want to customize the default serializer, you can configure the `ProblemDetailsSerializationOptions` class, like this: ```csharp -services.AddExceptionMapper(builder => builder - .MapCommonHttpExceptionHandlers() -); +builder.Services.Configure(options => +{ + options.SerializeExceptions = false; + options.DisplayDebugInformation = (ExceptionHandlingContext ctx) => + { +#if DEBUG + return true; +#else + return false; +#endif + }; +}); ``` -You can also configure the `FallbackExceptionHandlerOptions` during the registration or like any other options: +You can also customize the options from the `appsettings.json` file: -```csharp -services.AddExceptionMapper(builder => builder - .MapCommonHttpExceptionHandlers(options => - { - options.Strategy = FallbackStrategy.Handle; - }) -); -// OR using the Asp.Net Core options pattern like: -services.Configure(options => +```json { - options.Strategy = FallbackStrategy.Handle; -}); + "ExceptionMapper": { + "ProblemDetailsSerialization": { + "SerializeExceptions": false + } + } +} ``` -# ForEvolve.ExceptionMapper.FluentMapper - -This package contains utilities that can be used to program exception's mapping in the Startup class, without creating any new type. +Note that the serializer displays the debug information when in development. +Use the `DisplayDebugInformation` function to display the debug info in other environment, like staging or production. -For example, to map a `MyUnauthorizedException` to a status code 401, we could do the following: +## Property names -```csharp -services.AddExceptionMapper(builder => builder - .Map(map => map.ToStatusCode(401)) -); -``` +To change the way the property name are serialized, you can configure the `JsonOptions` and change the `PropertyNamingPolicy` property. -The `MyUnauthorizedException` looks as simple as this: +**.NET 8+** ```csharp -public class MyUnauthorizedException : Exception { } +builder.Services.ConfigureHttpJsonOptions(options => { + options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseUpper; +}); ``` -The `Map()` extension has been designed to be fluent, so it returns an `IExceptionMappingBuilder`, allowing us to chain multiple calls. We can also mix and match with any other configuration helpers as they all do that. - -For example, we could do the following: +**.NET 6 and .NET 7** ```csharp -services.AddExceptionMapper(builder => builder - .MapCommonHttpExceptionHandlers() - .AddExceptionHandler() - .Map(map => map.ToStatusCode(401)) - .Map(map => map.ToStatusCode(410)) -); +builder.Services.Configure(options => { + options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; +}); ``` -Under the hood, the `Map()` extension creates a `FluentExceptionHandler` that is configurable. You can append, prepend or replace handler actions. +### Dictionary keys -# ForEvolve.ExceptionMapper.Serialization.Json +For .NET 7+ projects, ExceptionMapper sets the `DictionaryKeyPolicy` property to the `PropertyNamingPolicy` property value so dictionaries are serialized the same way as the normal properties. -This package contains a handler that serializes exceptions as `ProblemDetails`. +## Ensuring a property is not serialized -The following serializes all exception using the default options: +If your custom exception has properties that should not be serialized, you can mark them with the `[JsonIgnore]` attribute, like the following and the serializer will ignore them: ```csharp -services.AddExceptionMapper(builder => builder - // ... - .SerializeAsProblemDetails() -); -``` - -## Configuring the serialization handler - -It is possible to configure the `ProblemDetailsSerializationOptions` like any other options, or using the following extension method: - -```csharp -public class Startup +public class MyForbiddenException : Exception { - public IConfiguration Configuration { get; } - // ... - public void ConfigureServices(IServiceCollection services) + public MyForbiddenException() + : base("Accessing this resource is forbidden.") { - services.AddExceptionMapper(builder => builder - // ... - .SerializeAsProblemDetails(Configuration.GetSection("ExceptionMapper")) - ); + } - // ... -} -``` -Here is an example of `appsettings.json`: + public string CustomProperty1 => "Lorem Ipsum 1"; -```json -{ - "ExceptionMapper": { - "SerializeExceptions": true, - "ContentType": "application/problem+json", - "JsonSerializerOptions": { - "IgnoreNullValues": true - // ... - } - } + [JsonIgnore] + public string CustomProperty2 => "Lorem Ipsum 2"; } ``` -Another overload is to pass an instance of `ProblemDetailsSerializationOptions` directly like this: +## Creating your own serializer + +If you want to replace the default serializer, implement the `IExceptionSerializer` and register it with the container, before calling `AddExceptionMapper`: ```csharp -public void ConfigureServices(IServiceCollection services) -{ - var options = new ProblemDetailsSerializationOptions(); - services.AddExceptionMapper(builder => builder - // ... - .SerializeAsProblemDetails(options) - ); -} +services.AddSingleton(); +builder.AddExceptionMapper(); ``` # Release notes +## 3.0 + +The version 3 of ExceptionMapper is a major rewrite that simplifies the codebase and usage of the library. Here are a few important changes: + +- Add support to .NET 7 and .NET 8. +- Remove transitive dependency on JSON.NET (`Newtonsoft.Json`). +- Drop support for .NET Standard 2.0 because `ExceptionMapper` depends on the `HttpContext` class which requires a `` which is not compatible with `netstandard2.0`. +- Merge all assemblies in `ForEvolve.ExceptionMapper` but `ForEvolve.ExceptionMapper.Scrutor` and removed `ForEvolve.ExceptionMapper.Scrutor` althogether. +- Replace the `AddMvcCore` call by registering a copy of the `DefaultProblemDetailsFactory` using a `TryAddSingleton` call, so you must register your custom `ProblemDetailsFactory` implementation before `AddExceptionMapper`. The good news is, if you are using a custom factory, the `ProblemDetailsSerializationHandler` will use it! + > Removing the copy of the `DefaultProblemDetailsFactory` class could be resolved by https://github.com/dotnet/aspnetcore/issues/49982 +- Calling `AddExceptionMapper()` now registers the common exceptions and the serializer automatically. +- The `Order` property was removed from the `IExceptionHandler` interface. The system uses the registration order instead. +- The interface now leverage a serialzier implementing the `IExceptionSerializer` interface. The serializer no longer implements the `IExceptionHandler` interface. +- By default, `ProblemDetailsSerializationOptions` is bound to the section `ExceptionMapper:ProblemDetailsSerialization` and `FallbackExceptionHandlerOptions` is bound to the section `ExceptionMapper:FallbackExceptionHandler`. + +### Breaking changes .NET 7+ + +- Remove the `ContentType` and `JsonSerializerOptions` properties from the `ProblemDetailsSerializationOptions` class (`ForEvolve.ExceptionMapper.Serialization.Json`). +- The `ProblemDetailsSerializationHandler` class now leverages the `IProblemDetailsService` interface to write the `ProblemDetails` object to the response stream instead of serializing it with the `JsonSerializer`, relinguishing the control of the process to .NET. +- The `ProblemDetailsSerializationHandler` leverages the `JsonOptions` class to ensure the names are formatted according to the `PropertyNamingPolicy` object. The default is `camelCase`. +- ExceptionMapper sets the `DictionaryKeyPolicy` property to the `PropertyNamingPolicy` property value so dictionaries are serialized the same way as the normal properties. + ## 2.0 - Drop .NET Core 3.1 support @@ -319,10 +331,9 @@ public void ConfigureServices(IServiceCollection services) Here is a list of what I want to do: -- [x] Take the fallback out of `MapCommonHttpExceptions()` into its own extension, like `MapHttpFallback()` -- [x] Add one or more serialization handlers that at least support JSON serialization and that leverage `ProblemDetailsFactory` to create `ProblemDetails` objects. -- [ ] Write tests that covers `ForEvolve.ExceptionMapper.FluentMapper` and other missing pieces -- [ ] Create a MVC/Razor Pages filter that could replace the middleware or work in conjunction of it, adding more control over the process for MVC applications. _This may not be important._ +- [ ] Improve overall test coverage. +- [ ] Implement custom type converter. The serializer would either use the converter or fall back to the reflection-based code if no type converter is available. +- [ ] Create a more "real-life" code sample. # Found a bug or have a feature request? diff --git a/samples/WebApi.HttpMiddleware/ExceptionController.cs b/samples/WebApi.HttpMiddleware/ExceptionController.cs index 34d6f6c..4f4fec5 100644 --- a/samples/WebApi.HttpMiddleware/ExceptionController.cs +++ b/samples/WebApi.HttpMiddleware/ExceptionController.cs @@ -3,63 +3,85 @@ using System; using System.ComponentModel.DataAnnotations; -namespace WebApi.Shared +namespace WebApi.Shared; + +[ApiController] +[Route("mvc")] +public class ExceptionController : ControllerBase { - [ApiController] - [Route("mvc")] - public class ExceptionController : ControllerBase - { #pragma warning disable IDE0022 // Use block body for methods - [HttpGet("NotFound")] - public IActionResult NotFoundException() => throw new NotFoundException(); + [HttpGet("BadRequestException")] + public IActionResult BadRequestException() => throw new BadRequestException(); + + [HttpGet("ConflictException")] + public IActionResult ConflictException() => throw new ConflictException(); + + [HttpGet("ForbiddenException")] + public IActionResult ForbiddenException() => throw new ForbiddenException(); + + [HttpGet("GoneException")] + public IActionResult GoneException() => throw new GoneException(); + + [HttpGet("NotFoundException")] + public IActionResult NotFoundException() => throw new NotFoundException(); + + [HttpGet("ResourceNotFoundException")] + public IActionResult ResourceNotFoundException() => throw new ResourceNotFoundException(HttpContext); + + [HttpGet("UnauthorizedException")] + public IActionResult UnauthorizedException() => throw new UnauthorizedException(); + + - [HttpGet("Conflict")] - public IActionResult ConflictException() => throw new ConflictException(); + [HttpGet("GatewayTimeoutException")] + public IActionResult GatewayTimeoutException() => throw new GatewayTimeoutException(); - [HttpGet("InternalServerError")] - public IActionResult InternalServerError() => throw new InternalServerErrorException(new Exception()); + [HttpGet("InternalServerError")] + public IActionResult InternalServerError() => throw new InternalServerErrorException(new Exception()); - [HttpGet("NotImplemented")] - public IActionResult NotImplemented() => throw new NotImplementedException(); + [HttpGet("ServiceUnavailableException")] + public IActionResult ServiceUnavailableException() => throw new ServiceUnavailableException(); - [HttpGet("MyNotFoundException")] - public IActionResult MyNotFoundException() => throw new MyNotFoundException(); - [HttpGet("Fallback")] - public IActionResult Fallback() => throw new Exception(); - [HttpGet("MyUnauthorizedException")] - public IActionResult MyUnauthorizedException() => throw new MyUnauthorizedException(); + [HttpGet("ImATeapotException")] + public IActionResult ImATeapotException() => throw new ImATeapotException(); - [HttpGet("GoneException")] - public IActionResult GoneException() => throw new GoneException(); + [HttpGet("MyForbiddenException")] + public IActionResult MyForbiddenException() => throw new MyForbiddenException(); - [HttpGet("ImATeapotException")] - public IActionResult ImATeapotException() => throw new ImATeapotException(); + [HttpGet("DroidNotFoundException")] + public IActionResult DroidNotFoundException() => throw new DroidNotFoundException(); + + [HttpGet("MyUnauthorizedException")] + public IActionResult MyUnauthorizedException() => throw new MyUnauthorizedException(Random.Shared.Next(100) % 2 == 0 ? "John" : "Jane"); + + + + [HttpGet("Fallback")] + public IActionResult Fallback() => throw new Exception("An error that gets handled by the fallback handler."); - [HttpGet("MyForbiddenException")] - public IActionResult MyForbiddenException() => throw new MyForbiddenException(); #pragma warning restore IDE0022 // Use block body for methods - [HttpGet("ValidationError")] - public IActionResult ValidationError([FromQuery]MyModel model) + [HttpGet("ValidationError")] + public IActionResult ValidationError([FromQuery] MyModel model) + { + if (!ModelState.IsValid) { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - return Ok(new { - message = "This should not happen, please review the controller action." - }); + return BadRequest(ModelState); } - - public class MyModel + return Ok(new { - [Required] - public string Name { get; set; } + message = "This should not happen, please review the controller action." + }); + } - [Range(18, 35)] - public int Age { get; set; } - } + public class MyModel + { + [Required] + public string Name { get; set; } + + [Range(18, 35)] + public int Age { get; set; } } } diff --git a/samples/WebApi.HttpMiddleware/Program.cs b/samples/WebApi.HttpMiddleware/Program.cs index 221212b..e3688e8 100644 --- a/samples/WebApi.HttpMiddleware/Program.cs +++ b/samples/WebApi.HttpMiddleware/Program.cs @@ -1,26 +1,19 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -namespace WebApi.HttpMiddleware +namespace WebApi.HttpMiddleware; + +public class Program { - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); + CreateHostBuilder(args).Build().Run(); } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); } diff --git a/samples/WebApi.HttpMiddleware/Startup.cs b/samples/WebApi.HttpMiddleware/Startup.cs index a55a5b3..a1cd409 100644 --- a/samples/WebApi.HttpMiddleware/Startup.cs +++ b/samples/WebApi.HttpMiddleware/Startup.cs @@ -1,114 +1,125 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading.Tasks; using ForEvolve.ExceptionMapper; -using ForEvolve.ExceptionMapper.Handlers.Fallback; -using ForEvolve.ExceptionMapper.Serialization.Json; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using System; using WebApi.Shared; -namespace WebApi.HttpMiddleware +namespace WebApi.HttpMiddleware; + +public class Startup { - public class Startup + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) { - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) + services + .AddExceptionMapper(Configuration, builder => + { + builder.AddExceptionHandler(); + builder.Map().ToStatusCode(StatusCodes.Status418ImATeapot); + }) + .AddControllers() + .ConfigureApiBehaviorOptions(options => + { + options.ClientErrorMapping[StatusCodes.Status409Conflict].Link = "https://localhost:8828/Status409Conflict"; + }); + ; + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) { - services - .AddExceptionMapper(builder => builder - .AddExceptionHandler() - .AddExceptionHandler() - .MapCommonHttpExceptions() - .MapHttpFallback(options => - { - options.Strategy = FallbackStrategy.Handle; - }) - .Map(map => map.ToStatusCode(401)) - .Map(map => map.ToStatusCode(410)) - .Map(map => map.To(context => - { - context.HttpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; - context.Result = new ExceptionHandledResult(context.Error); - context.HttpContext.Response.WriteAsync("{\"title\":\"This operation is not supported at the moment!\"}"); - return Task.CompletedTask; - }, ForEvolve.ExceptionMapper.FluentMapper.FluentHandlerStrategy.Append)) - .SerializeAsProblemDetails() - ) - .AddControllers() - .ConfigureApiBehaviorOptions(options => - { - options.ClientErrorMapping[StatusCodes.Status409Conflict].Link = "https://localhost:8828/Status409Conflict"; - }); - ; + app.UseDeveloperExceptionPage(); } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + app.UseRouting(); + app.UseExceptionMapper(); + app.UseEndpoints(endpoints => { - if (env.IsDevelopment()) + endpoints.MapControllers(); + endpoints.MapGet("/", () => new { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - app.UseExceptionMapper(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - endpoints.MapGet("/", async context => + routing = new string[] + { + "---[Client]---", + "/Routing/BadRequestException", + "/Routing/ConflictException", + "/Routing/ForbiddenException", + "/Routing/GoneException", + "/Routing/NotFoundException", + "/Routing/ResourceNotFoundException", + "/Routing/UnauthorizedException", + "---[Server]---", + "/Routing/GatewayTimeoutException", + "/Routing/InternalServerErrorException", + "/Routing/ServiceUnavailableException", + "---[Custom]---", + "/Routing/ImATeapotException", + "/Routing/MyForbiddenException", + "/Routing/DroidNotFoundException", + "/Routing/MyUnauthorizedException", + "---[Others]---", + "/Routing/fallback", + }, + mvc = new string[] { - var baseUri = $"{context.Request.Scheme}://{context.Request.Host}"; - context.Response.ContentType = "application/json"; - await context.Response.WriteAsync("{"); - await context.Response.WriteAsync("\"name\":\"Exploration document\","); - await context.Response.WriteAsync("\"routing\":["); - await context.Response.WriteAsync($"\"{baseUri}/Routing/NotFound\","); - await context.Response.WriteAsync($"\"{baseUri}/Routing/Conflict\","); - await context.Response.WriteAsync($"\"{baseUri}/Routing/InternalServerError\","); - await context.Response.WriteAsync($"\"{baseUri}/Routing/NotImplemented\","); - await context.Response.WriteAsync($"\"{baseUri}/Routing/MyNotFoundException\","); - await context.Response.WriteAsync($"\"{baseUri}/Routing/Fallback\","); - await context.Response.WriteAsync($"\"{baseUri}/Routing/MyUnauthorizedException\","); - await context.Response.WriteAsync($"\"{baseUri}/Routing/GoneException\","); - await context.Response.WriteAsync($"\"{baseUri}/Routing/ImATeapotException\","); - await context.Response.WriteAsync($"\"{baseUri}/Routing/MyForbiddenException\""); - await context.Response.WriteAsync("],"); - await context.Response.WriteAsync("\"mvc\":["); - await context.Response.WriteAsync($"\"{baseUri}/mvc/NotFound\","); - await context.Response.WriteAsync($"\"{baseUri}/mvc/Conflict\","); - await context.Response.WriteAsync($"\"{baseUri}/mvc/InternalServerError\","); - await context.Response.WriteAsync($"\"{baseUri}/mvc/NotImplemented\","); - await context.Response.WriteAsync($"\"{baseUri}/mvc/MyNotFoundException\","); - await context.Response.WriteAsync($"\"{baseUri}/mvc/Fallback\","); - await context.Response.WriteAsync($"\"{baseUri}/mvc/MyUnauthorizedException\","); - await context.Response.WriteAsync($"\"{baseUri}/mvc/GoneException\","); - await context.Response.WriteAsync($"\"{baseUri}/mvc/ImATeapotException\","); - await context.Response.WriteAsync($"\"{baseUri}/mvc/MyForbiddenException\","); - await context.Response.WriteAsync($"\"{baseUri}/mvc/ValidationError\""); - await context.Response.WriteAsync("]"); - await context.Response.WriteAsync("}"); - }); - endpoints.MapGet("/Routing/NotFound", context => throw new NotFoundException()); - endpoints.MapGet("/Routing/Conflict", context => throw new ConflictException()); - endpoints.MapGet("/Routing/InternalServerError", context => throw new InternalServerErrorException(new Exception())); - endpoints.MapGet("/Routing/NotImplemented", context => throw new NotImplementedException()); - endpoints.MapGet("/Routing/MyNotFoundException", context => throw new MyNotFoundException()); - endpoints.MapGet("/Routing/Fallback", context => throw new Exception()); - endpoints.MapGet("/Routing/MyUnauthorizedException", context => throw new MyUnauthorizedException()); - endpoints.MapGet("/Routing/GoneException", context => throw new GoneException()); - endpoints.MapGet("/Routing/ImATeapotException", context => throw new ImATeapotException()); - endpoints.MapGet("/Routing/MyForbiddenException", context => throw new MyForbiddenException()); + "---[Client]---", + "/mvc/BadRequestException", + "/mvc/ConflictException", + "/mvc/ForbiddenException", + "/mvc/GoneException", + "/mvc/NotFoundException", + "/mvc/ResourceNotFoundException", + "/mvc/UnauthorizedException", + "---[Server]---", + "/mvc/GatewayTimeoutException", + "/mvc/InternalServerErrorException", + "/mvc/ServiceUnavailableException", + "---[Custom]---", + "/mvc/ImATeapotException", + "/mvc/MyForbiddenException", + "/mvc/DroidNotFoundException", + "/mvc/MyUnauthorizedException", + "---[Others]---", + "/mvc/fallback", + "/mvc/ValidationError" + }, + other = new string[] + { + "/a-url-that-does-not-exist" + } }); - } + + endpoints.MapGet("/Routing/BadRequestException", context => throw new BadRequestException()); + endpoints.MapGet("/Routing/ConflictException", context => throw new ConflictException()); + endpoints.MapGet("/Routing/ForbiddenException", context => throw new ForbiddenException()); + endpoints.MapGet("/Routing/GoneException", context => throw new GoneException()); + endpoints.MapGet("/Routing/NotFoundException", context => throw new NotFoundException()); + endpoints.MapGet("/Routing/ResourceNotFoundException", context => throw new ResourceNotFoundException(context)); + endpoints.MapGet("/Routing/UnauthorizedException", context => throw new UnauthorizedException()); + + endpoints.MapGet("/Routing/GatewayTimeoutException", context => throw new GatewayTimeoutException()); + endpoints.MapGet("/Routing/InternalServerErrorException", context => throw new InternalServerErrorException(new Exception("Some other error that occurred."))); + endpoints.MapGet("/Routing/ServiceUnavailableException", context => throw new ServiceUnavailableException()); + + endpoints.MapGet("/Routing/ImATeapotException", context => throw new ImATeapotException()); + endpoints.MapGet("/Routing/MyForbiddenException", context => throw new MyForbiddenException()); + endpoints.MapGet("/Routing/DroidNotFoundException", context => throw new DroidNotFoundException()); + endpoints.MapGet("/Routing/MyUnauthorizedException", context => throw new MyUnauthorizedException(Random.Shared.Next(100) % 2 == 0 ? "John" : "Jane")); + + endpoints.MapGet("/Routing/Fallback", context => throw new Exception("An error that gets handled by the fallback handler.")); + }); } } diff --git a/samples/WebApi.HttpMiddleware/WebApi.HttpMiddleware.csproj b/samples/WebApi.HttpMiddleware/WebApi.HttpMiddleware.csproj index d87ac44..f014969 100644 --- a/samples/WebApi.HttpMiddleware/WebApi.HttpMiddleware.csproj +++ b/samples/WebApi.HttpMiddleware/WebApi.HttpMiddleware.csproj @@ -1,12 +1,10 @@  - net5.0;net6.0 + $(FETestsTargetFrameworks) - - diff --git a/samples/WebApi.Minimal/Program.cs b/samples/WebApi.Minimal/Program.cs new file mode 100644 index 0000000..c811bfb --- /dev/null +++ b/samples/WebApi.Minimal/Program.cs @@ -0,0 +1,123 @@ +#undef CHANGE_PROPERTY_NAME_POLICY +#undef CONFIGURE_OPTIONS +using FluentValidation; +using ForEvolve.ExceptionMapper; +using ForEvolve.ExceptionMapper.Serialization.Json; +using Microsoft.AspNetCore.Mvc; +using System.Text.Json; +using WebApi.Shared; + +var builder = WebApplication.CreateBuilder(args); +#if CHANGE_PROPERTY_NAME_POLICY +#if NET8_0_OR_GREATER +builder.Services.ConfigureHttpJsonOptions(options => { + options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseUpper; +}); +#else +builder.Services.Configure(options => { + options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; +}); +#endif +#endif +builder.AddExceptionMapper(builder => +{ + builder.AddExceptionHandler(); + builder.Map().ToStatusCode(StatusCodes.Status418ImATeapot); +}); +builder.Services + .Configure(options => + { + options.ClientErrorMapping.Add(StatusCodes.Status409Conflict, new ClientErrorData + { + Link = "https://localhost:8828/Status409Conflict", // This is taken into account because the middleware do not set any link by default. + Title = "This will not be displayed." // Not taken into account because the middleware sets the title to the Exception.Message value. + }); + }) +; + +#if CONFIGURE_OPTIONS +builder.Services.Configure(options => +{ + options.SerializeExceptions = false; + options.DisplayDebugInformation = (ExceptionHandlingContext ctx) => + { +#if DEBUG + return true; +#else + return false; +#endif + }; +}); +#endif + +var app = builder.Build(); +app.UseExceptionMapper(); +app.MapGet("/", () => new string[] +{ + "---[Client]---", + "/BadRequestException", + "/ConflictException", + "/ForbiddenException", + "/GoneException", + "/NotFoundException", + "/ResourceNotFoundException", + "/UnauthorizedException", + "---[Server]---", + "/GatewayTimeoutException", + "/InternalServerErrorException", + "/ServiceUnavailableException", + "---[Custom]---", + "/ImATeapotException", + "/MyForbiddenException", + "/DroidNotFoundException", + "/MyUnauthorizedException", + "---[Others]---", + "/fallback", + "/a-url-that-does-not-exist", +#if NET7_0_OR_GREATER + "/fluent-validation?name=&description=&range=0", +#endif +}); +app.MapGet("/BadRequestException", context => throw new BadRequestException()); +app.MapGet("/ConflictException", context => throw new ConflictException()); +app.MapGet("/ForbiddenException", context => throw new ForbiddenException()); +app.MapGet("/GoneException", context => throw new GoneException()); +app.MapGet("/NotFoundException", context => throw new NotFoundException()); +app.MapGet("/ResourceNotFoundException", context => throw new ResourceNotFoundException(context)); +app.MapGet("/UnauthorizedException", context => throw new UnauthorizedException()); + +app.MapGet("/GatewayTimeoutException", context => throw new GatewayTimeoutException()); +app.MapGet("/InternalServerErrorException", context => throw new InternalServerErrorException(new Exception("Some other error that occurred."))); +app.MapGet("/ServiceUnavailableException", context => throw new ServiceUnavailableException()); + +app.MapGet("/ImATeapotException", context => throw new ImATeapotException()); +app.MapGet("/MyForbiddenException", context => throw new MyForbiddenException()); +app.MapGet("/DroidNotFoundException", context => throw new DroidNotFoundException()); +app.MapGet("/MyUnauthorizedException", context => throw new MyUnauthorizedException(Random.Shared.Next(100) % 2 == 0 ? "John" : "Jane")); + +app.MapGet("/fallback", context => throw new Exception("An error that gets handled by the fallback handler.")); +#if NET7_0_OR_GREATER +app.MapGet("/fluent-validation", ([AsParameters] Entity entity) => +{ + var validator = new EntityValidator(); + var result = validator.Validate(entity); + if (!result.IsValid) + { + throw new ValidationException(result.Errors); + } + return TypedResults.Ok(entity); +}); +#endif +app.Run(); + + +public record class Entity(string Name, string Description, int Range); +public class EntityValidator : AbstractValidator +{ + public EntityValidator() + { + RuleFor(x => x.Name).NotEmpty(); + RuleFor(x => x.Description).NotEmpty(); + RuleFor(x => x.Range).GreaterThanOrEqualTo(20).LessThanOrEqualTo(40); + } +} \ No newline at end of file diff --git a/samples/WebApi.Minimal/Properties/launchSettings.json b/samples/WebApi.Minimal/Properties/launchSettings.json new file mode 100644 index 0000000..24fca77 --- /dev/null +++ b/samples/WebApi.Minimal/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:12239", + "sslPort": 44317 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5104", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7127;http://localhost:5104", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/WebApi.Minimal/WebApi.Minimal.csproj b/samples/WebApi.Minimal/WebApi.Minimal.csproj new file mode 100644 index 0000000..8debf13 --- /dev/null +++ b/samples/WebApi.Minimal/WebApi.Minimal.csproj @@ -0,0 +1,17 @@ + + + + $(FETestsTargetFrameworks) + enable + enable + + + + + + + + + + + diff --git a/samples/WebApi.Minimal/appsettings.Development.json b/samples/WebApi.Minimal/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/samples/WebApi.Minimal/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/WebApi.Mvc/appsettings.json b/samples/WebApi.Minimal/appsettings.json similarity index 56% rename from samples/WebApi.Mvc/appsettings.json rename to samples/WebApi.Minimal/appsettings.json index d9d9a9b..10f68b8 100644 --- a/samples/WebApi.Mvc/appsettings.json +++ b/samples/WebApi.Minimal/appsettings.json @@ -2,8 +2,7 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" diff --git a/samples/WebApi.Mvc/Program.cs b/samples/WebApi.Mvc/Program.cs deleted file mode 100644 index 502be32..0000000 --- a/samples/WebApi.Mvc/Program.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace WebApi.Mvc -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } -} diff --git a/samples/WebApi.Mvc/Properties/launchSettings.json b/samples/WebApi.Mvc/Properties/launchSettings.json deleted file mode 100644 index 6e1ad3b..0000000 --- a/samples/WebApi.Mvc/Properties/launchSettings.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "profiles": { - "WebApi.HttpMiddleware (watch)": { - "executablePath": "dotnet.exe", - "workingDirectory": "$(ProjectDir)", - "commandLineArgs": "watch run", - "applicationUrl": "https://localhost:9327;http://localhost:9326", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "WebApi.HttpMiddleware": { - "commandName": "Project", - "launchBrowser": true, - "applicationUrl": "https://localhost:9327;http://localhost:9326", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "WebApi.HttpMiddleware (Production)": { - "commandName": "Project", - "launchBrowser": true, - "applicationUrl": "https://localhost:9327;http://localhost:9326", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Production" - } - } - } -} diff --git a/samples/WebApi.Mvc/Startup.cs b/samples/WebApi.Mvc/Startup.cs deleted file mode 100644 index 1d06f73..0000000 --- a/samples/WebApi.Mvc/Startup.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace WebApi.Mvc -{ - public class Startup - { - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) - { - //services.AddExceptionMapper(builder => builder - // .MapCommonMvcExceptions() - //); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapGet("/", async context => - { - await context.Response.WriteAsync("Hello World!"); - }); - }); - } - } -} diff --git a/samples/WebApi.Mvc/WebApi.Mvc.csproj b/samples/WebApi.Mvc/WebApi.Mvc.csproj deleted file mode 100644 index 2e6c72a..0000000 --- a/samples/WebApi.Mvc/WebApi.Mvc.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - net5.0;net6.0 - - - - - - - - diff --git a/samples/WebApi.Mvc/appsettings.Development.json b/samples/WebApi.Mvc/appsettings.Development.json deleted file mode 100644 index 8983e0f..0000000 --- a/samples/WebApi.Mvc/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - } -} diff --git a/samples/WebApi.Shared/DroidNotFoundException.cs b/samples/WebApi.Shared/DroidNotFoundException.cs new file mode 100644 index 0000000..f79aa74 --- /dev/null +++ b/samples/WebApi.Shared/DroidNotFoundException.cs @@ -0,0 +1,12 @@ +using ForEvolve.ExceptionMapper; + +namespace WebApi.Shared; + +public class DroidNotFoundException : NotFoundException +{ + public DroidNotFoundException() + : base("These aren't the droids we're looking for.") + { + + } +} diff --git a/samples/WebApi.Shared/GoneException.cs b/samples/WebApi.Shared/GoneException.cs deleted file mode 100644 index 906b291..0000000 --- a/samples/WebApi.Shared/GoneException.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace WebApi.Shared -{ - public class GoneException : Exception - { - } -} diff --git a/samples/WebApi.Shared/ImATeapotException.cs b/samples/WebApi.Shared/ImATeapotException.cs index d7f880e..2bd5923 100644 --- a/samples/WebApi.Shared/ImATeapotException.cs +++ b/samples/WebApi.Shared/ImATeapotException.cs @@ -1,9 +1,8 @@ using System; -namespace WebApi.Shared +namespace WebApi.Shared; + +public class ImATeapotException : Exception { - public class ImATeapotException : Exception - { - } } diff --git a/samples/WebApi.Shared/ImATeapotExceptionHandler.cs b/samples/WebApi.Shared/ImATeapotExceptionHandler.cs deleted file mode 100644 index 1383a00..0000000 --- a/samples/WebApi.Shared/ImATeapotExceptionHandler.cs +++ /dev/null @@ -1,34 +0,0 @@ -using ForEvolve.ExceptionMapper; -using Microsoft.AspNetCore.Http; -using System; -using System.Threading.Tasks; - -namespace WebApi.Shared -{ - public class ImATeapotExceptionHandler : IExceptionHandler - { - public int Order => HandlerOrder.DefaultOrder; - - public async Task ExecuteAsync(ExceptionHandlingContext context) - { - var response = context.HttpContext.Response; - response.StatusCode = StatusCodes.Status418ImATeapot; - response.ContentType = "text/html"; - context.Result = new ExceptionHandledResult(context.Error); - await response.WriteAsync("
");
-            await response.WriteAsync(@"             ;,'
-     _o_    ;:;'
- ,-.'---`.__ ;
-((j`=====',-'
- `-\     /
-    `-=-'     hjw
-Source: https://www.asciiart.eu/food-and-drinks/coffee-and-tea");
-            await response.WriteAsync("
"); - } - - public Task KnowHowToHandleAsync(Exception exception) - { - return Task.FromResult(exception is ImATeapotException); - } - } -} diff --git a/samples/WebApi.Shared/MyForbiddenException.cs b/samples/WebApi.Shared/MyForbiddenException.cs index 9e8e63f..d66530a 100644 --- a/samples/WebApi.Shared/MyForbiddenException.cs +++ b/samples/WebApi.Shared/MyForbiddenException.cs @@ -1,9 +1,18 @@ using System; +using System.Text.Json.Serialization; -namespace WebApi.Shared +namespace WebApi.Shared; + +public class MyForbiddenException : Exception { - public class MyForbiddenException : Exception + public MyForbiddenException() + : base("Accessing this resource is forbidden.") { - + } + + public string CustomProperty1 => "Lorem Ipsum 1"; + + [JsonIgnore] + public string CustomProperty2 => "Lorem Ipsum 2"; } diff --git a/samples/WebApi.Shared/MyForbiddenExceptionHandler.cs b/samples/WebApi.Shared/MyForbiddenExceptionHandler.cs index 14507e5..6684444 100644 --- a/samples/WebApi.Shared/MyForbiddenExceptionHandler.cs +++ b/samples/WebApi.Shared/MyForbiddenExceptionHandler.cs @@ -1,10 +1,9 @@ using ForEvolve.ExceptionMapper; using Microsoft.AspNetCore.Http; -namespace WebApi.Shared +namespace WebApi.Shared; + +public class MyForbiddenExceptionHandler : ExceptionHandler { - public class MyForbiddenExceptionHandler : ExceptionHandler - { - public override int StatusCode => StatusCodes.Status403Forbidden; - } + public override int StatusCode => StatusCodes.Status403Forbidden; } diff --git a/samples/WebApi.Shared/MyNotFoundException.cs b/samples/WebApi.Shared/MyNotFoundException.cs deleted file mode 100644 index 3880d2a..0000000 --- a/samples/WebApi.Shared/MyNotFoundException.cs +++ /dev/null @@ -1,8 +0,0 @@ -using ForEvolve.ExceptionMapper; - -namespace WebApi.Shared -{ - public class MyNotFoundException : NotFoundException - { - } -} diff --git a/samples/WebApi.Shared/MyUnauthorizedException.cs b/samples/WebApi.Shared/MyUnauthorizedException.cs index 0f63270..d462866 100644 --- a/samples/WebApi.Shared/MyUnauthorizedException.cs +++ b/samples/WebApi.Shared/MyUnauthorizedException.cs @@ -1,8 +1,12 @@ -using System; +using ForEvolve.ExceptionMapper; -namespace WebApi.Shared +namespace WebApi.Shared; + +public class MyUnauthorizedException : UnauthorizedException { - public class MyUnauthorizedException : Exception + public MyUnauthorizedException(string name) + : base($"Sorry {name}, you can't access this page.") { + } } diff --git a/samples/WebApi.Shared/WebApi.Shared.csproj b/samples/WebApi.Shared/WebApi.Shared.csproj index e42da42..1e6ab37 100644 --- a/samples/WebApi.Shared/WebApi.Shared.csproj +++ b/samples/WebApi.Shared/WebApi.Shared.csproj @@ -1,12 +1,12 @@ - + - net5.0;net6.0 - False + $(FETestsTargetFrameworks) + False - + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 35115d1..33a40bc 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -12,11 +12,13 @@ true True snupkg + enable + enable - + - 3.2.31 + 3.6.133 all diff --git a/src/ForEvolve.ExceptionMapper.CommonExceptions/ClientErrorException.cs b/src/ForEvolve.ExceptionMapper.CommonExceptions/ClientErrorException.cs deleted file mode 100644 index 4f4f73c..0000000 --- a/src/ForEvolve.ExceptionMapper.CommonExceptions/ClientErrorException.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace ForEvolve.ExceptionMapper -{ - public abstract class ClientErrorException : ForEvolveException - { - public ClientErrorException() - { - } - - public ClientErrorException(string message) : base(message) - { - } - - public ClientErrorException(string message, Exception innerException) : base(message, innerException) - { - } - } -} diff --git a/src/ForEvolve.ExceptionMapper.CommonExceptions/ClientErrors/BadRequestException.cs b/src/ForEvolve.ExceptionMapper.CommonExceptions/ClientErrors/BadRequestException.cs deleted file mode 100644 index 4c56393..0000000 --- a/src/ForEvolve.ExceptionMapper.CommonExceptions/ClientErrors/BadRequestException.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; - -namespace ForEvolve.ExceptionMapper -{ - public class BadRequestException : Exception - { - public BadRequestException() - { - } - - public BadRequestException(string message) : base(message) - { - } - - public BadRequestException(string message, Exception innerException) : base(message, innerException) - { - } - } -} diff --git a/src/ForEvolve.ExceptionMapper.CommonExceptions/ClientErrors/ConflictException.cs b/src/ForEvolve.ExceptionMapper.CommonExceptions/ClientErrors/ConflictException.cs deleted file mode 100644 index 61bfd5b..0000000 --- a/src/ForEvolve.ExceptionMapper.CommonExceptions/ClientErrors/ConflictException.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace ForEvolve.ExceptionMapper -{ - public class ConflictException : ClientErrorException - { - public ConflictException() - { - } - - public ConflictException(string message) : base(message) - { - } - - public ConflictException(string message, Exception innerException) : base(message, innerException) - { - } - } -} diff --git a/src/ForEvolve.ExceptionMapper.CommonExceptions/ClientErrors/ForbiddenException.cs b/src/ForEvolve.ExceptionMapper.CommonExceptions/ClientErrors/ForbiddenException.cs deleted file mode 100644 index c79cd49..0000000 --- a/src/ForEvolve.ExceptionMapper.CommonExceptions/ClientErrors/ForbiddenException.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace ForEvolve.ExceptionMapper -{ - public class ForbiddenException : Exception - { - public ForbiddenException() - { - } - - public ForbiddenException(string message) : base(message) - { - } - - public ForbiddenException(string message, Exception innerException) : base(message, innerException) - { - } - } -} diff --git a/src/ForEvolve.ExceptionMapper.CommonExceptions/ClientErrors/NotFoundException.cs b/src/ForEvolve.ExceptionMapper.CommonExceptions/ClientErrors/NotFoundException.cs deleted file mode 100644 index a4f672f..0000000 --- a/src/ForEvolve.ExceptionMapper.CommonExceptions/ClientErrors/NotFoundException.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace ForEvolve.ExceptionMapper -{ - public class NotFoundException : ClientErrorException - { - public NotFoundException() - { - } - - public NotFoundException(string message) : base(message) - { - } - - public NotFoundException(string message, Exception innerException) : base(message, innerException) - { - } - } -} diff --git a/src/ForEvolve.ExceptionMapper.CommonExceptions/ClientErrors/ResourceNotFoundException.cs b/src/ForEvolve.ExceptionMapper.CommonExceptions/ClientErrors/ResourceNotFoundException.cs deleted file mode 100644 index a965bdc..0000000 --- a/src/ForEvolve.ExceptionMapper.CommonExceptions/ClientErrors/ResourceNotFoundException.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; - -namespace ForEvolve.ExceptionMapper -{ - public class ResourceNotFoundException : NotFoundException - { - public ResourceNotFoundException(HttpContext context) - : base($"No resource were found at '{context.Request.GetDisplayUrl()}'.") - { - - } - } -} diff --git a/src/ForEvolve.ExceptionMapper.CommonExceptions/ClientErrors/UnauthorizedException.cs b/src/ForEvolve.ExceptionMapper.CommonExceptions/ClientErrors/UnauthorizedException.cs deleted file mode 100644 index 7f380a2..0000000 --- a/src/ForEvolve.ExceptionMapper.CommonExceptions/ClientErrors/UnauthorizedException.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; - -namespace ForEvolve.ExceptionMapper -{ - public class UnauthorizedException : Exception - { - public UnauthorizedException() - { - } - - public UnauthorizedException(string message) : base(message) - { - } - - public UnauthorizedException(string message, Exception innerException) : base(message, innerException) - { - } - } -} diff --git a/src/ForEvolve.ExceptionMapper.CommonExceptions/ForEvolve.ExceptionMapper.CommonExceptions.csproj b/src/ForEvolve.ExceptionMapper.CommonExceptions/ForEvolve.ExceptionMapper.CommonExceptions.csproj deleted file mode 100644 index 3ede149..0000000 --- a/src/ForEvolve.ExceptionMapper.CommonExceptions/ForEvolve.ExceptionMapper.CommonExceptions.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - netstandard2.0 - ForEvolve.ExceptionMapper - - - - - - - - diff --git a/src/ForEvolve.ExceptionMapper.CommonExceptions/ServerErrorException.cs b/src/ForEvolve.ExceptionMapper.CommonExceptions/ServerErrorException.cs deleted file mode 100644 index 3b7dda1..0000000 --- a/src/ForEvolve.ExceptionMapper.CommonExceptions/ServerErrorException.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace ForEvolve.ExceptionMapper -{ - public abstract class ServerErrorException : ForEvolveException - { - public ServerErrorException() - { - } - - public ServerErrorException(string message) : base(message) - { - } - - public ServerErrorException(string message, Exception innerException) : base(message, innerException) - { - } - } -} diff --git a/src/ForEvolve.ExceptionMapper.CommonExceptions/ServerErrors/InternalServerErrorException.cs b/src/ForEvolve.ExceptionMapper.CommonExceptions/ServerErrors/InternalServerErrorException.cs deleted file mode 100644 index e90ce30..0000000 --- a/src/ForEvolve.ExceptionMapper.CommonExceptions/ServerErrors/InternalServerErrorException.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace ForEvolve.ExceptionMapper -{ - public class InternalServerErrorException : ServerErrorException - { - public InternalServerErrorException(Exception innerException) - : base(innerException.Message, innerException) - { - - } - } -} diff --git a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/CommonExceptionsMappingBuilderExtensions.cs b/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/CommonExceptionsMappingBuilderExtensions.cs deleted file mode 100644 index 5a07bc7..0000000 --- a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/CommonExceptionsMappingBuilderExtensions.cs +++ /dev/null @@ -1,38 +0,0 @@ -using ForEvolve.ExceptionMapper; -using ForEvolve.ExceptionMapper.Handlers; -using ForEvolve.ExceptionMapper.Handlers.Fallback; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.Extensions.DependencyInjection -{ - public static class CommonHttpExceptionHandlersMappingBuilderExtensions - { - /// - /// Registers all found in the assembly - /// with singleton lifetime. - /// - public static IExceptionMappingBuilder MapCommonHttpExceptions(this IExceptionMappingBuilder builder) - { - return builder.ScanHandlersFrom( - s => s - .FromAssembliesOf(typeof(CommonHttpExceptionHandlersMappingBuilderExtensions)) - .AddClasses(x => x.InExactNamespaceOf()), - ServiceLifetime.Singleton - ); - } - - /// - /// Registers all found in the assembly - /// with singleton lifetime. - /// - public static IExceptionMappingBuilder MapHttpFallback(this IExceptionMappingBuilder builder, Action setup = null) - { - if (setup != null) { builder.Services.Configure(setup); } - return builder.AddExceptionHandler(); - } - } -} diff --git a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Fallback/FallbackExceptionHandler.cs b/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Fallback/FallbackExceptionHandler.cs deleted file mode 100644 index 80dc502..0000000 --- a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Fallback/FallbackExceptionHandler.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; -using System; -using System.Threading.Tasks; - -namespace ForEvolve.ExceptionMapper.Handlers.Fallback -{ - public class FallbackExceptionHandler : IExceptionHandler - { - public int Order => HandlerOrder.FallbackOrder; - - private readonly FallbackExceptionHandlerOptions _options; - public FallbackExceptionHandler(IOptionsMonitor options) - { - _options = options.CurrentValue; - } - - public Task KnowHowToHandleAsync(Exception exception) - { - if (_options.Strategy == FallbackStrategy.Handle) - { - return Task.FromResult(true); - } - return Task.FromResult(false); - } - - public Task ExecuteAsync(ExceptionHandlingContext context) - { - if (_options.Strategy == FallbackStrategy.Handle) - { - if (!context.Result.ExceptionHandled) - { - context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; - context.Result = new ExceptionHandledResult(context.Error); - } - } - return Task.CompletedTask; - } - } -} diff --git a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Fallback/FallbackExceptionHandlerOptions.cs b/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Fallback/FallbackExceptionHandlerOptions.cs deleted file mode 100644 index bd2e315..0000000 --- a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Fallback/FallbackExceptionHandlerOptions.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ForEvolve.ExceptionMapper.Handlers.Fallback -{ - public class FallbackExceptionHandlerOptions - { - public FallbackStrategy Strategy { get; set; } - } -} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Fallback/FallbackStrategy.cs b/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Fallback/FallbackStrategy.cs deleted file mode 100644 index 13e2e83..0000000 --- a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Fallback/FallbackStrategy.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace ForEvolve.ExceptionMapper.Handlers.Fallback -{ - public enum FallbackStrategy - { - Ignore = 0, - Handle = 1 - } -} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.csproj b/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.csproj deleted file mode 100644 index 61e3287..0000000 --- a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - - netstandard2.0 - - - - - - - diff --git a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Handlers/BadRequestExceptionHandler.cs b/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Handlers/BadRequestExceptionHandler.cs deleted file mode 100644 index 571baf6..0000000 --- a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Handlers/BadRequestExceptionHandler.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.AspNetCore.Http; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace ForEvolve.ExceptionMapper.Handlers -{ - public class BadRequestExceptionHandler : ExceptionHandler - { - public override int StatusCode => StatusCodes.Status400BadRequest; - } -} diff --git a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Handlers/ConflictExceptionHandler.cs b/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Handlers/ConflictExceptionHandler.cs deleted file mode 100644 index 2e86912..0000000 --- a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Handlers/ConflictExceptionHandler.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Microsoft.AspNetCore.Http; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace ForEvolve.ExceptionMapper.Handlers -{ - public class ConflictExceptionHandler : ExceptionHandler - { - public override int StatusCode => StatusCodes.Status409Conflict; - } -} diff --git a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Handlers/ForbiddenExceptionHandler.cs b/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Handlers/ForbiddenExceptionHandler.cs deleted file mode 100644 index cca6651..0000000 --- a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Handlers/ForbiddenExceptionHandler.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace ForEvolve.ExceptionMapper.Handlers -{ - public class ForbiddenExceptionHandler : ExceptionHandler - { - public override int StatusCode => StatusCodes.Status403Forbidden; - } -} diff --git a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Handlers/InternalServerErrorExceptionHandler.cs b/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Handlers/InternalServerErrorExceptionHandler.cs deleted file mode 100644 index 24c48aa..0000000 --- a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Handlers/InternalServerErrorExceptionHandler.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace ForEvolve.ExceptionMapper.Handlers -{ - public class InternalServerErrorExceptionHandler : ExceptionHandler - { - public override int StatusCode => StatusCodes.Status500InternalServerError; - } -} diff --git a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Handlers/NotFoundExceptionHandler.cs b/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Handlers/NotFoundExceptionHandler.cs deleted file mode 100644 index 4571e3a..0000000 --- a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Handlers/NotFoundExceptionHandler.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace ForEvolve.ExceptionMapper.Handlers -{ - public class NotFoundExceptionHandler : ExceptionHandler - { - public override int StatusCode => StatusCodes.Status404NotFound; - } -} diff --git a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Handlers/NotImplementedExceptionHandler.cs b/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Handlers/NotImplementedExceptionHandler.cs deleted file mode 100644 index 95c9e56..0000000 --- a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Handlers/NotImplementedExceptionHandler.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Microsoft.AspNetCore.Http; -using System; - -namespace ForEvolve.ExceptionMapper.Handlers -{ - public class NotImplementedExceptionHandler : ExceptionHandler - { - public override int StatusCode => StatusCodes.Status501NotImplemented; - } -} diff --git a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Handlers/UnauthorizedExceptionHandler.cs b/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Handlers/UnauthorizedExceptionHandler.cs deleted file mode 100644 index 2be796d..0000000 --- a/src/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers/Handlers/UnauthorizedExceptionHandler.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace ForEvolve.ExceptionMapper.Handlers -{ - public class UnauthorizedExceptionHandler : ExceptionHandler - { - public override int StatusCode => StatusCodes.Status401Unauthorized; - } -} diff --git a/src/ForEvolve.ExceptionMapper.Core/DependencyInjection/ExceptionMappingBuilderExtensions.cs b/src/ForEvolve.ExceptionMapper.Core/DependencyInjection/ExceptionMappingBuilderExtensions.cs deleted file mode 100644 index 765780b..0000000 --- a/src/ForEvolve.ExceptionMapper.Core/DependencyInjection/ExceptionMappingBuilderExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using ForEvolve.ExceptionMapper; - -namespace Microsoft.Extensions.DependencyInjection -{ - public static class ExceptionMappingBuilderExtensions - { - public static IExceptionMappingBuilder AddExceptionHandler(this IExceptionMappingBuilder builder) - where THandler : class, IExceptionHandler - { - builder.Services.AddSingleton(); - return builder; - } - - public static IExceptionMappingBuilder AddExceptionHandler(this IExceptionMappingBuilder builder, THandler handler) - where THandler : class, IExceptionHandler - { - builder.Services.AddSingleton(handler); - return builder; - } - } -} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper.Core/DependencyInjection/IExceptionMappingBuilder.cs b/src/ForEvolve.ExceptionMapper.Core/DependencyInjection/IExceptionMappingBuilder.cs deleted file mode 100644 index 397880c..0000000 --- a/src/ForEvolve.ExceptionMapper.Core/DependencyInjection/IExceptionMappingBuilder.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Microsoft.Extensions.DependencyInjection -{ - public interface IExceptionMappingBuilder - { - IServiceCollection Services { get; } - } -} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper.Core/DependencyInjection/ServiceCollectionExtensions.cs b/src/ForEvolve.ExceptionMapper.Core/DependencyInjection/ServiceCollectionExtensions.cs deleted file mode 100644 index 371f1a7..0000000 --- a/src/ForEvolve.ExceptionMapper.Core/DependencyInjection/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using ForEvolve.ExceptionMapper; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; -using System; - -namespace Microsoft.Extensions.DependencyInjection -{ - public static class ServiceCollectionExceptionFiltersExtensions - { - public static IServiceCollection AddExceptionMapper(this IServiceCollection services, Action exceptionMappingBuilder = null) - { - services.AddLogging(); - exceptionMappingBuilder?.Invoke(new ServiceCollectionWrapper(services)); - services.TryAddSingleton(); - return services; - } - } -} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper.Core/DependencyInjection/ServiceCollectionWrapper.cs b/src/ForEvolve.ExceptionMapper.Core/DependencyInjection/ServiceCollectionWrapper.cs deleted file mode 100644 index f707b10..0000000 --- a/src/ForEvolve.ExceptionMapper.Core/DependencyInjection/ServiceCollectionWrapper.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace Microsoft.Extensions.DependencyInjection -{ - public class ServiceCollectionWrapper : IExceptionMappingBuilder - { - public ServiceCollectionWrapper(IServiceCollection services) - { - Services = services ?? throw new ArgumentNullException(nameof(services)); - } - - public IServiceCollection Services { get; } - } -} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper.Core/ExceptionHandler.cs b/src/ForEvolve.ExceptionMapper.Core/ExceptionHandler.cs deleted file mode 100644 index 653d3a1..0000000 --- a/src/ForEvolve.ExceptionMapper.Core/ExceptionHandler.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.AspNetCore.Http; -using System; -using System.Threading.Tasks; -namespace ForEvolve.ExceptionMapper -{ - public abstract class ExceptionHandler : IExceptionHandler - where TException : Exception - { - public abstract int StatusCode { get; } - public virtual int Order => HandlerOrder.DefaultOrder; - - public virtual Task KnowHowToHandleAsync(Exception exception) - { - return Task.FromResult(exception is TException); - } - - public virtual Task ExecuteAsync(ExceptionHandlingContext context) - { - context.HttpContext.Response.StatusCode = StatusCode; - context.Result = new ExceptionHandledResult(context.Error); - return ExecuteCoreAsync(new ExceptionHandlingContext(context)); - } - - protected virtual Task ExecuteCoreAsync(ExceptionHandlingContext context) - { - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper.Core/ExceptionHandlingManager.cs b/src/ForEvolve.ExceptionMapper.Core/ExceptionHandlingManager.cs deleted file mode 100644 index e629d7b..0000000 --- a/src/ForEvolve.ExceptionMapper.Core/ExceptionHandlingManager.cs +++ /dev/null @@ -1,90 +0,0 @@ -using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.Http; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Threading.Tasks; -namespace ForEvolve.ExceptionMapper -{ - public class ExceptionHandlingManager : IExceptionHandlingManager - { - private readonly List _handlers; - - public ExceptionHandlingManager(IEnumerable handlers) - { - if (handlers == null) { throw new ArgumentNullException(nameof(handlers)); } - - _handlers = handlers - .OrderBy(x => x.Order) - .ToList(); - } - - public IReadOnlyCollection Handlers - => new ReadOnlyCollection(_handlers); - - public async Task HandleAsync(HttpContext httpContext) - { - var exceptionHandlerPathFeature = httpContext.Features.Get(); - if (exceptionHandlerPathFeature == null) - { - return new ExceptionHandlerFeatureNotSupportedResult(); - } - - var exception = exceptionHandlerPathFeature.Error; - if (exception == null) - { - return new NoExceptionResult(); - } - - var context = new ExceptionHandlingContext(httpContext, exception, new ExceptionNotHandledResult(exception)); - foreach (var handler in _handlers) - { - if (await handler.KnowHowToHandleAsync(exception)) - { - await handler.ExecuteAsync(context); - } - } - - return context.Result; - } - } - - public class ExceptionHandlingContext - where TException : Exception - { - public ExceptionHandlingContext(ExceptionHandlingContext previous) - : this(previous.HttpContext, previous.Error as TException, previous.Result) - { - } - - public ExceptionHandlingContext(ExceptionHandlingContext previous) - : this(previous.HttpContext, previous.Error, previous.Result) - { - } - - public ExceptionHandlingContext(HttpContext httpContext, TException error, IExceptionHandlingResult initialResult) - { - HttpContext = httpContext ?? throw new ArgumentNullException(nameof(httpContext)); - Error = error ?? throw new ArgumentNullException(nameof(error)); - Result = initialResult; - } - - public HttpContext HttpContext { get; } - public TException Error { get; } - public IExceptionHandlingResult Result { get; set; } - } - - public class ExceptionHandlingContext : ExceptionHandlingContext - { - public ExceptionHandlingContext(ExceptionHandlingContext previous) - : base(previous) - { - } - - public ExceptionHandlingContext(HttpContext httpContext, Exception error, IExceptionHandlingResult initialResult) - : base(httpContext, error, initialResult) - { - } - } -} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper.Core/ForEvolve.ExceptionMapper.Core.csproj b/src/ForEvolve.ExceptionMapper.Core/ForEvolve.ExceptionMapper.Core.csproj deleted file mode 100644 index 2649f0d..0000000 --- a/src/ForEvolve.ExceptionMapper.Core/ForEvolve.ExceptionMapper.Core.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - netstandard2.0 - - - - - - - - - - - diff --git a/src/ForEvolve.ExceptionMapper.Core/HandlerOrder.cs b/src/ForEvolve.ExceptionMapper.Core/HandlerOrder.cs deleted file mode 100644 index e39b5cf..0000000 --- a/src/ForEvolve.ExceptionMapper.Core/HandlerOrder.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace ForEvolve.ExceptionMapper -{ - public sealed class HandlerOrder - { - public const int DefaultOrder = 1; - public const int FallbackOrder = 50; - public const int SerializerOrder = 100; - } -} diff --git a/src/ForEvolve.ExceptionMapper.Core/IExceptionHandler.cs b/src/ForEvolve.ExceptionMapper.Core/IExceptionHandler.cs deleted file mode 100644 index 42b8086..0000000 --- a/src/ForEvolve.ExceptionMapper.Core/IExceptionHandler.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Microsoft.AspNetCore.Http; -using System; -using System.Threading.Tasks; - -namespace ForEvolve.ExceptionMapper -{ - public interface IExceptionHandler - { - int Order { get; } - Task KnowHowToHandleAsync(Exception exception); - Task ExecuteAsync(ExceptionHandlingContext context); - } -} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper.Core/IExceptionHandlingManager.cs b/src/ForEvolve.ExceptionMapper.Core/IExceptionHandlingManager.cs deleted file mode 100644 index 24c5149..0000000 --- a/src/ForEvolve.ExceptionMapper.Core/IExceptionHandlingManager.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.AspNetCore.Http; -using System.Threading.Tasks; -namespace ForEvolve.ExceptionMapper -{ - public interface IExceptionHandlingManager - { - Task HandleAsync(HttpContext context); - } -} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper.Core/Results/ExceptionHandledResult.cs b/src/ForEvolve.ExceptionMapper.Core/Results/ExceptionHandledResult.cs deleted file mode 100644 index ea0212d..0000000 --- a/src/ForEvolve.ExceptionMapper.Core/Results/ExceptionHandledResult.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace ForEvolve.ExceptionMapper -{ - public class ExceptionHandledResult : IExceptionHandlingResult - { - public ExceptionHandledResult(Exception error) - { - Error = error ?? throw new ArgumentNullException(nameof(error)); - ExceptionHandled = true; - ExceptionHandlerFeatureSupported = true; - } - - public bool ExceptionHandled { get; } - public Exception Error { get; } - public bool ExceptionHandlerFeatureSupported { get; } - } -} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper.Core/Results/ExceptionHandlerFeatureNotSupportedResult.cs b/src/ForEvolve.ExceptionMapper.Core/Results/ExceptionHandlerFeatureNotSupportedResult.cs deleted file mode 100644 index 2100b06..0000000 --- a/src/ForEvolve.ExceptionMapper.Core/Results/ExceptionHandlerFeatureNotSupportedResult.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace ForEvolve.ExceptionMapper -{ - public sealed class ExceptionHandlerFeatureNotSupportedResult : IExceptionHandlingResult - { - public bool ExceptionHandled { get; } - public Exception Error { get; } - public bool ExceptionHandlerFeatureSupported => false; - } -} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper.Core/Results/ExceptionNotHandledResult.cs b/src/ForEvolve.ExceptionMapper.Core/Results/ExceptionNotHandledResult.cs deleted file mode 100644 index cf08590..0000000 --- a/src/ForEvolve.ExceptionMapper.Core/Results/ExceptionNotHandledResult.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace ForEvolve.ExceptionMapper -{ - public class ExceptionNotHandledResult : IExceptionHandlingResult - { - public ExceptionNotHandledResult(Exception error) - { - Error = error ?? throw new ArgumentNullException(nameof(error)); - ExceptionHandlerFeatureSupported = true; - } - - public bool ExceptionHandled { get; } - public Exception Error { get; } - public bool ExceptionHandlerFeatureSupported { get; } - } -} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper.Core/Results/IExceptionHandlingResult.cs b/src/ForEvolve.ExceptionMapper.Core/Results/IExceptionHandlingResult.cs deleted file mode 100644 index 5c105fe..0000000 --- a/src/ForEvolve.ExceptionMapper.Core/Results/IExceptionHandlingResult.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace ForEvolve.ExceptionMapper -{ - public interface IExceptionHandlingResult - { - bool ExceptionHandled { get; } - Exception Error { get; } - bool ExceptionHandlerFeatureSupported { get; } - } -} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper.Core/Results/NoExceptionResult.cs b/src/ForEvolve.ExceptionMapper.Core/Results/NoExceptionResult.cs deleted file mode 100644 index 8c2eaa7..0000000 --- a/src/ForEvolve.ExceptionMapper.Core/Results/NoExceptionResult.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace ForEvolve.ExceptionMapper -{ - public class NoExceptionResult : IExceptionHandlingResult - { - public NoExceptionResult() - { - ExceptionHandlerFeatureSupported = true; - } - - public bool ExceptionHandled { get; } - public Exception Error { get; } - public bool ExceptionHandlerFeatureSupported { get; } - } -} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper.FluentMapper/FluentExceptionHandler.cs b/src/ForEvolve.ExceptionMapper.FluentMapper/FluentExceptionHandler.cs deleted file mode 100644 index eb8a7ea..0000000 --- a/src/ForEvolve.ExceptionMapper.FluentMapper/FluentExceptionHandler.cs +++ /dev/null @@ -1,69 +0,0 @@ -using ForEvolve.ExceptionMapper; -using System; -using System.Threading.Tasks; - -namespace ForEvolve.ExceptionMapper.FluentMapper -{ - public class FluentExceptionHandler : IExceptionHandler - where TException : Exception - { - public int Order { get; set; } = HandlerOrder.DefaultOrder; - private Func _handler; - - public Task ExecuteAsync(ExceptionHandlingContext context) - { - if (_handler == null) - { - return Task.CompletedTask; - } - var task = _handler?.Invoke(context); - if (!context.Result.ExceptionHandled) - { - context.Result = new ExceptionHandledResult(context.Error); - } - return task; - } - - public Task KnowHowToHandleAsync(Exception exception) - { - return Task.FromResult(exception is TException); - } - - public void AppendHandler(Func handler) - { - if (handler == null) { throw new ArgumentNullException(nameof(handler)); } - if (_handler == null) - { - _handler = handler; - return; - } - _handler = (context) => - { - var task1 = _handler.Invoke(context); - var task2 = handler.Invoke(context); - return Task.WhenAll(task1, task2); - }; - } - - public void PrependHandler(Func handler) - { - if (handler == null) { throw new ArgumentNullException(nameof(handler)); } - if (_handler == null) - { - _handler = handler; - return; - } - _handler = (context) => - { - var task1 = handler.Invoke(context); - var task2 = _handler.Invoke(context); - return Task.WhenAll(task1, task2); - }; - } - - public void ReplaceHandler(Func handler) - { - _handler = handler ?? throw new ArgumentNullException(nameof(handler)); - } - } -} diff --git a/src/ForEvolve.ExceptionMapper.FluentMapper/FluentHandlerStrategy.cs b/src/ForEvolve.ExceptionMapper.FluentMapper/FluentHandlerStrategy.cs deleted file mode 100644 index 5406a71..0000000 --- a/src/ForEvolve.ExceptionMapper.FluentMapper/FluentHandlerStrategy.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ForEvolve.ExceptionMapper.FluentMapper -{ - public enum FluentHandlerStrategy - { - Append, - Prepend, - Replace - } -} diff --git a/src/ForEvolve.ExceptionMapper.FluentMapper/FluentMapperBuilder.cs b/src/ForEvolve.ExceptionMapper.FluentMapper/FluentMapperBuilder.cs deleted file mode 100644 index 72ef875..0000000 --- a/src/ForEvolve.ExceptionMapper.FluentMapper/FluentMapperBuilder.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using System; - -namespace ForEvolve.ExceptionMapper.FluentMapper -{ - public class FluentMapperBuilder - where TException : Exception - { - public FluentMapperBuilder(IExceptionMappingBuilder builder, FluentExceptionHandler handler) - { - Builder = builder ?? throw new ArgumentNullException(nameof(builder)); - Handler = handler ?? throw new ArgumentNullException(nameof(handler)); - } - - public IExceptionMappingBuilder Builder { get; } - - public IServiceCollection Services => Builder.Services; - - public FluentExceptionHandler Handler { get; } - } -} diff --git a/src/ForEvolve.ExceptionMapper.FluentMapper/FluentMapperBuilderExtensions.cs b/src/ForEvolve.ExceptionMapper.FluentMapper/FluentMapperBuilderExtensions.cs deleted file mode 100644 index f6f0ab1..0000000 --- a/src/ForEvolve.ExceptionMapper.FluentMapper/FluentMapperBuilderExtensions.cs +++ /dev/null @@ -1,45 +0,0 @@ -using ForEvolve.ExceptionMapper; -using ForEvolve.ExceptionMapper.FluentMapper; -using System; -using System.Threading.Tasks; - -namespace Microsoft.Extensions.DependencyInjection -{ - public static class FluentMapperBuilderExtensions - { - public static FluentMapperBuilder ToStatusCode(this FluentMapperBuilder fluentBuilder, int expectedStatusCode, FluentHandlerStrategy strategy = FluentHandlerStrategy.Replace) - where TException : Exception - { - return fluentBuilder.To( - context => StatusCodeHandler(context, expectedStatusCode), - strategy - ); - - static Task StatusCodeHandler(ExceptionHandlingContext context, int statusCode) - { - context.HttpContext.Response.StatusCode = statusCode; - return Task.CompletedTask; - } - } - - public static FluentMapperBuilder To(this FluentMapperBuilder fluentBuilder, Func handler, FluentHandlerStrategy strategy = FluentHandlerStrategy.Replace) - where TException : Exception - { - switch (strategy) - { - case FluentHandlerStrategy.Append: - fluentBuilder.Handler.AppendHandler(handler); - break; - case FluentHandlerStrategy.Prepend: - fluentBuilder.Handler.PrependHandler(handler); - break; - case FluentHandlerStrategy.Replace: - fluentBuilder.Handler.ReplaceHandler(handler); - break; - default: - throw new NotSupportedException($"The strategy '{strategy}' is not supported."); - } - return fluentBuilder; - } - } -} diff --git a/src/ForEvolve.ExceptionMapper.FluentMapper/FluentMapperExceptionMappingBuilderExtensions.cs b/src/ForEvolve.ExceptionMapper.FluentMapper/FluentMapperExceptionMappingBuilderExtensions.cs deleted file mode 100644 index 6d1a382..0000000 --- a/src/ForEvolve.ExceptionMapper.FluentMapper/FluentMapperExceptionMappingBuilderExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -using ForEvolve.ExceptionMapper.FluentMapper; -using System; - -namespace Microsoft.Extensions.DependencyInjection -{ - public static class FluentMapperExceptionMappingBuilderExtensions - { - public static IExceptionMappingBuilder Map(this IExceptionMappingBuilder builder, Action> mapperAction) - where TException : Exception - { - if (mapperAction == null) { throw new ArgumentNullException(nameof(mapperAction)); } - - var handler = new FluentExceptionHandler(); - var fluentMapper = new FluentMapperBuilder(builder, handler); - builder.AddExceptionHandler(handler); - mapperAction.Invoke(fluentMapper); - return builder; - } - } -} diff --git a/src/ForEvolve.ExceptionMapper.FluentMapper/ForEvolve.ExceptionMapper.FluentMapper.csproj b/src/ForEvolve.ExceptionMapper.FluentMapper/ForEvolve.ExceptionMapper.FluentMapper.csproj deleted file mode 100644 index 2a2dbc7..0000000 --- a/src/ForEvolve.ExceptionMapper.FluentMapper/ForEvolve.ExceptionMapper.FluentMapper.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - - netstandard2.0 - - - - - - - diff --git a/src/ForEvolve.ExceptionMapper.HttpMiddleware/ApplicationBuilderExtensions.cs b/src/ForEvolve.ExceptionMapper.HttpMiddleware/ApplicationBuilderExtensions.cs deleted file mode 100644 index 7238f06..0000000 --- a/src/ForEvolve.ExceptionMapper.HttpMiddleware/ApplicationBuilderExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using ForEvolve.ExceptionMapper; -namespace Microsoft.AspNetCore.Builder -{ - public static class ApplicationBuilderExtensions - { - public static IApplicationBuilder UseExceptionMapper(this IApplicationBuilder app) - { - app.UseExceptionHandler(errorApp => - { - errorApp.UseMiddleware(); - }); - app.UseMiddleware(); - return app; - } - } -} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper.HttpMiddleware/ForEvolve.ExceptionMapper.HttpMiddleware.csproj b/src/ForEvolve.ExceptionMapper.HttpMiddleware/ForEvolve.ExceptionMapper.HttpMiddleware.csproj deleted file mode 100644 index 1d73bd0..0000000 --- a/src/ForEvolve.ExceptionMapper.HttpMiddleware/ForEvolve.ExceptionMapper.HttpMiddleware.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - netstandard2.0 - - - - - - - - diff --git a/src/ForEvolve.ExceptionMapper.HttpMiddleware/HttpExceptionHandlingMiddleware.cs b/src/ForEvolve.ExceptionMapper.HttpMiddleware/HttpExceptionHandlingMiddleware.cs deleted file mode 100644 index 8acb2ac..0000000 --- a/src/ForEvolve.ExceptionMapper.HttpMiddleware/HttpExceptionHandlingMiddleware.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.AspNetCore.Http; -using System; -using System.Threading.Tasks; -namespace ForEvolve.ExceptionMapper -{ - public class HttpExceptionHandlingMiddleware - { - private readonly RequestDelegate _next; - private readonly IExceptionHandlingManager _exceptionHandlingManager; - - public HttpExceptionHandlingMiddleware(IExceptionHandlingManager exceptionHandlingManager, RequestDelegate next) - { - _exceptionHandlingManager = exceptionHandlingManager ?? throw new ArgumentNullException(nameof(exceptionHandlingManager)); - _next = next; - } - - public async Task InvokeAsync(HttpContext context) - { - var result = await _exceptionHandlingManager.HandleAsync(context); - if (result.ExceptionHandled) - { - return; - } - await _next(context); - } - } -} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper.HttpMiddleware/UnhandledStatusCodeException.cs b/src/ForEvolve.ExceptionMapper.HttpMiddleware/UnhandledStatusCodeException.cs deleted file mode 100644 index ed0bc13..0000000 --- a/src/ForEvolve.ExceptionMapper.HttpMiddleware/UnhandledStatusCodeException.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; -namespace ForEvolve.ExceptionMapper -{ - public class UnhandledStatusCodeException : Exception - { - public UnhandledStatusCodeException() - : base($"An unhandled error occured.") { } - } -} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper.HttpMiddleware/UnhandledStatusCodeMiddleware.cs b/src/ForEvolve.ExceptionMapper.HttpMiddleware/UnhandledStatusCodeMiddleware.cs deleted file mode 100644 index f0fc1d1..0000000 --- a/src/ForEvolve.ExceptionMapper.HttpMiddleware/UnhandledStatusCodeMiddleware.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Microsoft.AspNetCore.Http; -using System; -using System.Threading.Tasks; -namespace ForEvolve.ExceptionMapper -{ - public class UnhandledStatusCodeMiddleware - { - private readonly RequestDelegate _next; - - public UnhandledStatusCodeMiddleware(RequestDelegate next) => _next = next; - - public async Task InvokeAsync(HttpContext context) - { - await _next(context); - - if (context.Response.HasStarted) - { - return; - } - - // Client errors (400–499) - // Server errors (500–599) - if (context.Response.StatusCode >= 400) - { - switch (context.Response.StatusCode) - { - case StatusCodes.Status400BadRequest: - throw new BadRequestException(); - case StatusCodes.Status401Unauthorized: - throw new UnauthorizedException(); - case StatusCodes.Status403Forbidden: - throw new ForbiddenException(); - case StatusCodes.Status404NotFound: - throw new ResourceNotFoundException(context); - case StatusCodes.Status409Conflict: - throw new ConflictException(); - case StatusCodes.Status500InternalServerError: - throw new InternalServerErrorException(new UnhandledStatusCodeException()); - case StatusCodes.Status501NotImplemented: - throw new NotImplementedException(); - } - } - } - } -} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper.Mvc/Class1.cs b/src/ForEvolve.ExceptionMapper.Mvc/Class1.cs deleted file mode 100644 index d6143e6..0000000 --- a/src/ForEvolve.ExceptionMapper.Mvc/Class1.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace ForEvolve.ExceptionMapper.Mvc -{ - public class Class1 - { - } -} diff --git a/src/ForEvolve.ExceptionMapper.Mvc/ForEvolve.ExceptionMapper.Mvc.csproj b/src/ForEvolve.ExceptionMapper.Mvc/ForEvolve.ExceptionMapper.Mvc.csproj deleted file mode 100644 index 7599bbf..0000000 --- a/src/ForEvolve.ExceptionMapper.Mvc/ForEvolve.ExceptionMapper.Mvc.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - netstandard2.0 - False - - - - - - - diff --git a/src/ForEvolve.ExceptionMapper.Scrutor/AspNetExceptionMappingBuilderExtensions.cs b/src/ForEvolve.ExceptionMapper.Scrutor/AspNetExceptionMappingBuilderExtensions.cs deleted file mode 100644 index ff00407..0000000 --- a/src/ForEvolve.ExceptionMapper.Scrutor/AspNetExceptionMappingBuilderExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using ForEvolve.ExceptionMapper; -using Scrutor; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.Extensions.DependencyInjection -{ - public static class AspNetExceptionMappingBuilderExtensions - { - /// The type in which assembly that should be scanned. - public static IExceptionMappingBuilder ScanHandlersFromAssemblyOf(this IExceptionMappingBuilder builder, ServiceLifetime lifetime = ServiceLifetime.Singleton) - { - return builder.ScanHandlersFrom(s => s.FromAssemblyOf(), lifetime); - } - - public static IExceptionMappingBuilder ScanHandlersFrom(this IExceptionMappingBuilder builder, Func typeSelector, ServiceLifetime lifetime = ServiceLifetime.Singleton) - { - if (typeSelector == null) { throw new ArgumentNullException(nameof(typeSelector)); } - - builder.Services.Scan(s => typeSelector(s) - .AddClasses(f => f.AssignableTo()) - .As() - .WithLifetime(lifetime) - ); - return builder; - } - } -} diff --git a/src/ForEvolve.ExceptionMapper.Scrutor/ForEvolve.ExceptionMapper.Scrutor.csproj b/src/ForEvolve.ExceptionMapper.Scrutor/ForEvolve.ExceptionMapper.Scrutor.csproj deleted file mode 100644 index f69062f..0000000 --- a/src/ForEvolve.ExceptionMapper.Scrutor/ForEvolve.ExceptionMapper.Scrutor.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - netstandard2.0 - - - - - - - - - - - diff --git a/src/ForEvolve.ExceptionMapper.Serialization.Json/ForEvolve.ExceptionMapper.Serialization.Json.csproj b/src/ForEvolve.ExceptionMapper.Serialization.Json/ForEvolve.ExceptionMapper.Serialization.Json.csproj deleted file mode 100644 index 02d4dc3..0000000 --- a/src/ForEvolve.ExceptionMapper.Serialization.Json/ForEvolve.ExceptionMapper.Serialization.Json.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - netcoreapp3.1;net5.0;net6.0 - - - - - - - - - - - diff --git a/src/ForEvolve.ExceptionMapper.Serialization.Json/ProblemDetailsSerializationHandler.cs b/src/ForEvolve.ExceptionMapper.Serialization.Json/ProblemDetailsSerializationHandler.cs deleted file mode 100644 index e79e953..0000000 --- a/src/ForEvolve.ExceptionMapper.Serialization.Json/ProblemDetailsSerializationHandler.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; - -namespace ForEvolve.ExceptionMapper.Serialization.Json -{ - public class ProblemDetailsSerializationHandler : IExceptionHandler - { - private readonly ProblemDetailsFactory _problemDetailsFactory; - private readonly IHostEnvironment _hostEnvironment; - private readonly ProblemDetailsSerializationOptions _options; - - public ProblemDetailsSerializationHandler(ProblemDetailsFactory problemDetailsFactory, IHostEnvironment hostEnvironment, ProblemDetailsSerializationOptions options) - { - _problemDetailsFactory = problemDetailsFactory ?? throw new ArgumentNullException(nameof(problemDetailsFactory)); - _hostEnvironment = hostEnvironment ?? throw new ArgumentNullException(nameof(hostEnvironment)); - _options = options ?? throw new ArgumentNullException(nameof(options)); - } - - public int Order => HandlerOrder.SerializerOrder; - - public async Task ExecuteAsync(ExceptionHandlingContext ctx) - { - var problemDetails = _problemDetailsFactory.CreateProblemDetails( - ctx.HttpContext, - title: ctx.Error.Message, - statusCode: ctx.HttpContext.Response.StatusCode - ); - - var displayDebugInformation = _options.DisplayDebugInformation?.Invoke() ?? false; - if (displayDebugInformation || _hostEnvironment.IsDevelopment()) - { - var errorType = ctx.Error.GetType(); - problemDetails.Extensions.Add( - "debug", - new { - type = new { - name = errorType.Name, - fullName = errorType.FullName, - }, - stackTrace = ctx.Error.StackTrace, - } - ); - } - - ctx.HttpContext.Response.ContentType = _options.ContentType; - if(_options.JsonSerializerOptions is null) - { - await JsonSerializer.SerializeAsync( - ctx.HttpContext.Response.Body, - problemDetails - ); - } - else - { - await JsonSerializer.SerializeAsync( - ctx.HttpContext.Response.Body, - problemDetails, - _options.JsonSerializerOptions - ); - } - } - - public Task KnowHowToHandleAsync(Exception exception) - { - return Task.FromResult(_options.SerializeExceptions); - } - } -} diff --git a/src/ForEvolve.ExceptionMapper.Serialization.Json/ProblemDetailsSerializationOptions.cs b/src/ForEvolve.ExceptionMapper.Serialization.Json/ProblemDetailsSerializationOptions.cs deleted file mode 100644 index 2de1ad0..0000000 --- a/src/ForEvolve.ExceptionMapper.Serialization.Json/ProblemDetailsSerializationOptions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.Extensions.Options; -using System; -using System.Text.Json; - -namespace ForEvolve.ExceptionMapper.Serialization.Json -{ - public class ProblemDetailsSerializationOptions - { - public ProblemDetailsSerializationOptions() - { - SerializeExceptions = true; - DisplayDebugInformation = () => false; - ContentType = "application/problem+json; charset=utf-8"; - } - - public bool SerializeExceptions { get; set; } - public Func DisplayDebugInformation { get; set; } - public string ContentType { get; set; } - public JsonSerializerOptions JsonSerializerOptions { get; set; } - } -} diff --git a/src/ForEvolve.ExceptionMapper.Serialization.Json/SerializationJsonExtensions.cs b/src/ForEvolve.ExceptionMapper.Serialization.Json/SerializationJsonExtensions.cs deleted file mode 100644 index 3086730..0000000 --- a/src/ForEvolve.ExceptionMapper.Serialization.Json/SerializationJsonExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - -namespace ForEvolve.ExceptionMapper.Serialization.Json -{ - public static class SerializationJsonExtensions - { - public static IExceptionMappingBuilder SerializeAsProblemDetails(this IExceptionMappingBuilder builder) - => SerializeAsProblemDetails(builder, new ProblemDetailsSerializationOptions()); - - public static IExceptionMappingBuilder SerializeAsProblemDetails(this IExceptionMappingBuilder builder, ProblemDetailsSerializationOptions options) - { - builder.Services.AddSingleton(options); - return builder.SerializeAsProblemDetailsCore(); - } - - public static IExceptionMappingBuilder SerializeAsProblemDetails(this IExceptionMappingBuilder builder, IConfiguration configuration) - { - builder.Services - .Configure(configuration) - .AddSingleton(ctx => ctx.GetService>().CurrentValue) - ; - return builder.SerializeAsProblemDetailsCore(); - } - - private static IExceptionMappingBuilder SerializeAsProblemDetailsCore(this IExceptionMappingBuilder builder) - { - builder.Services.AddMvcCore(); // Workaround - return builder.AddExceptionHandler(); - } - - } -} diff --git a/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrorException.cs b/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrorException.cs new file mode 100644 index 0000000..8e11789 --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrorException.cs @@ -0,0 +1,20 @@ +namespace ForEvolve.ExceptionMapper; + +/// +/// Client error responses (400 – 499) +///

See also +///
+public abstract class ClientErrorException : ForEvolveException +{ + public ClientErrorException() + { + } + + public ClientErrorException(string message) : base(message) + { + } + + public ClientErrorException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrors/BadRequestException.cs b/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrors/BadRequestException.cs new file mode 100644 index 0000000..05c2979 --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrors/BadRequestException.cs @@ -0,0 +1,22 @@ +namespace ForEvolve.ExceptionMapper; + +/// +/// The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing). +///

See also +///
+/// 400 Bad Request +public class BadRequestException : ClientErrorException +{ + public BadRequestException() + : base("The server can not process the request due to a client error. Resolve the issue and try again.") + { + } + + public BadRequestException(string message) : base(message) + { + } + + public BadRequestException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrors/ConflictException.cs b/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrors/ConflictException.cs new file mode 100644 index 0000000..1df3694 --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrors/ConflictException.cs @@ -0,0 +1,22 @@ +namespace ForEvolve.ExceptionMapper; + +/// +/// This response is sent when a request conflicts with the current state of the server. +///

See also +///
+/// 409 Conflict +public class ConflictException : ClientErrorException +{ + public ConflictException() + : base("The request conflicts with the current state of the server.") + { + } + + public ConflictException(string message) : base(message) + { + } + + public ConflictException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrors/ForbiddenException.cs b/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrors/ForbiddenException.cs new file mode 100644 index 0000000..aab0bef --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrors/ForbiddenException.cs @@ -0,0 +1,22 @@ +namespace ForEvolve.ExceptionMapper; + +/// +/// The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server. +///

See also +///
+/// 403 Forbidden +public class ForbiddenException : ClientErrorException +{ + public ForbiddenException() + : base("You do not have access rights to the content.") + { + } + + public ForbiddenException(string message) : base(message) + { + } + + public ForbiddenException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrors/GoneException.cs b/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrors/GoneException.cs new file mode 100644 index 0000000..23a5343 --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrors/GoneException.cs @@ -0,0 +1,22 @@ +namespace ForEvolve.ExceptionMapper; + +/// +/// This response is sent when the requested content has been permanently deleted from server, with no forwarding address. Clients are expected to remove their caches and links to the resource. The HTTP specification intends this status code to be used for "limited-time, promotional services". APIs should not feel compelled to indicate resources that have been deleted with this status code. +///

See also +///
+/// 410 Gone +public class GoneException : ClientErrorException +{ + public GoneException() + : base("The requested content has been permanently deleted from server.") + { + } + + public GoneException(string message) : base(message) + { + } + + public GoneException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrors/NotFoundException.cs b/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrors/NotFoundException.cs new file mode 100644 index 0000000..5ab1ffd --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrors/NotFoundException.cs @@ -0,0 +1,22 @@ +namespace ForEvolve.ExceptionMapper; + +/// +/// The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web. +///

See also +///
+/// 404 Not Found +public class NotFoundException : ClientErrorException +{ + public NotFoundException() + : base("The server cannot find the requested resource.") + { + } + + public NotFoundException(string message) : base(message) + { + } + + public NotFoundException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrors/ResourceNotFoundException.cs b/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrors/ResourceNotFoundException.cs new file mode 100644 index 0000000..c59a37b --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrors/ResourceNotFoundException.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; + +namespace ForEvolve.ExceptionMapper; + +/// +/// The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. +///

See also +///
+/// 404 Not Found
A default error message is created using the requested URL.
+public class ResourceNotFoundException : NotFoundException +{ + public ResourceNotFoundException(HttpContext context) + : base($"The server cannot find the following resource: '{context.Request.GetDisplayUrl()}'.") + { + + } +} diff --git a/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrors/UnauthorizedException.cs b/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrors/UnauthorizedException.cs new file mode 100644 index 0000000..28489ea --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/CommonExceptions/ClientErrors/UnauthorizedException.cs @@ -0,0 +1,22 @@ +namespace ForEvolve.ExceptionMapper; + +/// +/// Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response. +///

See also +///
+/// 401 Unauthorized +public class UnauthorizedException : ClientErrorException +{ + public UnauthorizedException() + : base("You must be authenticated to access the requested resource.") + { + } + + public UnauthorizedException(string message) : base(message) + { + } + + public UnauthorizedException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/src/ForEvolve.ExceptionMapper/CommonExceptions/ServerErrorException.cs b/src/ForEvolve.ExceptionMapper/CommonExceptions/ServerErrorException.cs new file mode 100644 index 0000000..4c84abf --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/CommonExceptions/ServerErrorException.cs @@ -0,0 +1,20 @@ +namespace ForEvolve.ExceptionMapper; + +/// +/// Server error responses (500 – 599) +///

See also +///
+public abstract class ServerErrorException : ForEvolveException +{ + public ServerErrorException() + { + } + + public ServerErrorException(string message) : base(message) + { + } + + public ServerErrorException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/src/ForEvolve.ExceptionMapper/CommonExceptions/ServerErrors/GatewayTimeoutException.cs b/src/ForEvolve.ExceptionMapper/CommonExceptions/ServerErrors/GatewayTimeoutException.cs new file mode 100644 index 0000000..05615c0 --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/CommonExceptions/ServerErrors/GatewayTimeoutException.cs @@ -0,0 +1,22 @@ +namespace ForEvolve.ExceptionMapper; + +/// +/// This error response is given when the server is acting as a gateway and cannot get a response in time. +///

See also +///
+/// 504 Gateway Timeout +public class GatewayTimeoutException : ServerErrorException +{ + public GatewayTimeoutException() + : base("The server was not able to get the response in time.") + { + } + + public GatewayTimeoutException(string message) : base(message) + { + } + + public GatewayTimeoutException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper/CommonExceptions/ServerErrors/InternalServerErrorException.cs b/src/ForEvolve.ExceptionMapper/CommonExceptions/ServerErrors/InternalServerErrorException.cs new file mode 100644 index 0000000..4dccde8 --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/CommonExceptions/ServerErrors/InternalServerErrorException.cs @@ -0,0 +1,16 @@ +namespace ForEvolve.ExceptionMapper; + + +/// +/// The server has encountered a situation it does not know how to handle. +///

See also +///
+/// 500 Internal Server Error +public class InternalServerErrorException : ServerErrorException +{ + public InternalServerErrorException(Exception innerException) + : base(innerException.Message, innerException) + { + + } +} diff --git a/src/ForEvolve.ExceptionMapper/CommonExceptions/ServerErrors/ServiceUnavailableException.cs b/src/ForEvolve.ExceptionMapper/CommonExceptions/ServerErrors/ServiceUnavailableException.cs new file mode 100644 index 0000000..6b6cf4b --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/CommonExceptions/ServerErrors/ServiceUnavailableException.cs @@ -0,0 +1,22 @@ +namespace ForEvolve.ExceptionMapper; + +/// +/// The server is not ready to handle the request. Common causes are a server that is down for maintenance or that is overloaded. Note that together with this response, a user-friendly page explaining the problem should be sent. This response should be used for temporary conditions and the Retry-After HTTP header should, if possible, contain the estimated time before the recovery of the service. The webmaster must also take care about the caching-related headers that are sent along with this response, as these temporary condition responses should usually not be cached. +///

See also +///
+/// 503 Service Unavailable +public class ServiceUnavailableException : ServerErrorException +{ + public ServiceUnavailableException() + : base("The server is not ready to handle the request.") + { + } + + public ServiceUnavailableException(string message) : base(message) + { + } + + public ServiceUnavailableException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/src/ForEvolve.ExceptionMapper/CommonHttpExceptionHandlers/Fallback/FallbackExceptionHandler.cs b/src/ForEvolve.ExceptionMapper/CommonHttpExceptionHandlers/Fallback/FallbackExceptionHandler.cs new file mode 100644 index 0000000..2305f79 --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/CommonHttpExceptionHandlers/Fallback/FallbackExceptionHandler.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + +namespace ForEvolve.ExceptionMapper.Handlers.Fallback; + +public class FallbackExceptionHandler : IExceptionHandler +{ + private readonly FallbackExceptionHandlerOptions _options; + public FallbackExceptionHandler(IOptionsMonitor options) + { + _options = options.CurrentValue; + } + + public Task CanHandle(Exception exception) + { + if (_options.Strategy == FallbackStrategy.Handle) + { + return Task.FromResult(true); + } + return Task.FromResult(false); + } + + public Task ExecuteAsync(ExceptionHandlingContext context) + { + if (_options.Strategy == FallbackStrategy.Handle) + { + if (!context.Result.ExceptionHandled) + { + context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; + context.Result = new ExceptionHandledResult(context.Error); + } + } + return Task.CompletedTask; + } +} diff --git a/src/ForEvolve.ExceptionMapper/CommonHttpExceptionHandlers/Fallback/FallbackExceptionHandlerOptions.cs b/src/ForEvolve.ExceptionMapper/CommonHttpExceptionHandlers/Fallback/FallbackExceptionHandlerOptions.cs new file mode 100644 index 0000000..fc2a438 --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/CommonHttpExceptionHandlers/Fallback/FallbackExceptionHandlerOptions.cs @@ -0,0 +1,6 @@ +namespace ForEvolve.ExceptionMapper.Handlers.Fallback; + +public class FallbackExceptionHandlerOptions +{ + public FallbackStrategy Strategy { get; set; } = FallbackStrategy.Handle; +} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper/CommonHttpExceptionHandlers/Fallback/FallbackStrategy.cs b/src/ForEvolve.ExceptionMapper/CommonHttpExceptionHandlers/Fallback/FallbackStrategy.cs new file mode 100644 index 0000000..28874fd --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/CommonHttpExceptionHandlers/Fallback/FallbackStrategy.cs @@ -0,0 +1,7 @@ +namespace ForEvolve.ExceptionMapper.Handlers.Fallback; + +public enum FallbackStrategy +{ + Ignore = 0, + Handle = 1 +} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper/DependencyInjection/ApplicationBuilderExtensions.cs b/src/ForEvolve.ExceptionMapper/DependencyInjection/ApplicationBuilderExtensions.cs new file mode 100644 index 0000000..d3eaeb4 --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/DependencyInjection/ApplicationBuilderExtensions.cs @@ -0,0 +1,15 @@ +using ForEvolve.ExceptionMapper; +namespace Microsoft.AspNetCore.Builder; + +public static class ApplicationBuilderExtensions +{ + public static IApplicationBuilder UseExceptionMapper(this IApplicationBuilder app) + { + app.UseExceptionHandler(errorApp => + { + errorApp.UseMiddleware(); + }); + app.UseMiddleware(); + return app; + } +} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper/DependencyInjection/ServiceCollectionExtensions.cs b/src/ForEvolve.ExceptionMapper/DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..3972fe1 --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/DependencyInjection/ServiceCollectionExtensions.cs @@ -0,0 +1,125 @@ +using ForEvolve.ExceptionMapper; +using ForEvolve.ExceptionMapper.Handlers.Fallback; +using ForEvolve.ExceptionMapper.Serialization; +using ForEvolve.ExceptionMapper.Serialization.Json; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class ServiceCollectionExceptionFiltersExtensions +{ + public static IServiceCollection AddExceptionMapper(this IServiceCollection services, IConfiguration configuration, Action? exceptionMappingBuilder = null) + { + services.AddSingleton(); + services.AddSingleton(); + services.TryAddSingleton(); + services.AddSerializationHandler(configuration); + + var builder = new ExceptionMappingBuilder(services); + builder + // Common client exceptions + .Map().ToStatusCode(StatusCodes.Status400BadRequest) + .Map().ToStatusCode(StatusCodes.Status409Conflict) + .Map().ToStatusCode(StatusCodes.Status403Forbidden) + .Map().ToStatusCode(StatusCodes.Status410Gone) + .Map().ToStatusCode(StatusCodes.Status404NotFound) + .Map().ToStatusCode(StatusCodes.Status404NotFound) + .Map().ToStatusCode(StatusCodes.Status401Unauthorized) + + // .NET exceptions + .Map().ToStatusCode(StatusCodes.Status400BadRequest) + .Map().ToStatusCode(StatusCodes.Status501NotImplemented) + + // Common server exceptions + .Map().ToStatusCode(StatusCodes.Status504GatewayTimeout) + .Map().ToStatusCode(StatusCodes.Status500InternalServerError) + .Map().ToStatusCode(StatusCodes.Status503ServiceUnavailable) + ; + exceptionMappingBuilder?.Invoke(builder); + services.AddFallbackExceptionHandler(configuration, builder); + return services; + } + + public static WebApplicationBuilder AddExceptionMapper(this WebApplicationBuilder builder, Action? exceptionMappingBuilder = null) + { + AddExceptionMapper(builder.Services, builder.Configuration, exceptionMappingBuilder); + return builder; + } + + private static void AddSerializationHandler(this IServiceCollection services, IConfiguration configuration) + { +#if NET7_0_OR_GREATER + services.ConfigureHttpJsonOptions(options => { + options.SerializerOptions.DictionaryKeyPolicy = options.SerializerOptions.PropertyNamingPolicy; + }); +#endif + services + .AddOptions() + .Bind(configuration.GetSection("ExceptionMapper:ProblemDetailsSerialization")) + .ValidateOnStart() + ; + services.AddSingleton(sp => sp.GetRequiredService>().Value); +#if NET7_0_OR_GREATER + services.AddProblemDetails(); +#endif + // Workaround: binding a local copy of the DefaultProblemDetailsFactory because the .NET class is internal. + // Moreover, the only way to add the class is by calling the AddMvcCore method, which add way more services. + // So until we can add the DefaultProblemDetailsFactory + services.TryAddSingleton(); + services.TryAddSingleton(); + } + + private static void AddFallbackExceptionHandler(this IServiceCollection services, IConfiguration configuration, IExceptionMappingBuilder builder) + { + services + .AddOptions() + .Bind(configuration.GetSection("ExceptionMapper:FallbackExceptionHandler")) + .ValidateOnStart() + ; + services.AddSingleton(sp => sp.GetRequiredService>().Value); + builder.AddExceptionHandler(); + } + + public static Map Map(this IExceptionMappingBuilder builder) + where TException : Exception + { + return new Map(builder); + } + + public static IExceptionMappingBuilder ToStatusCode(this Map map, int expectedStatusCode) + where TException : Exception + { + map.Builder.Services.AddSingleton(new StatusCodeExceptionHandler(expectedStatusCode)); + return map.Builder; + } + + public static IExceptionMappingBuilder AddExceptionHandler(this IExceptionMappingBuilder builder) + where THandler : class, IExceptionHandler + { + builder.Services.AddSingleton(); + return builder; + } + + public static IExceptionMappingBuilder AddExceptionHandler(this IExceptionMappingBuilder builder, THandler handler) + where THandler : class, IExceptionHandler + { + builder.Services.AddSingleton(handler); + return builder; + } +} + +public class Map + where TException : Exception +{ + public Map(IExceptionMappingBuilder builder) + { + Builder = builder; + } + + public IExceptionMappingBuilder Builder { get; } +} diff --git a/src/ForEvolve.ExceptionMapper/DependencyInjection/ServiceCollectionWrapper.cs b/src/ForEvolve.ExceptionMapper/DependencyInjection/ServiceCollectionWrapper.cs new file mode 100644 index 0000000..2abe008 --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/DependencyInjection/ServiceCollectionWrapper.cs @@ -0,0 +1,14 @@ +using ForEvolve.ExceptionMapper; + +namespace Microsoft.Extensions.DependencyInjection; + +public class ExceptionMappingBuilder : IExceptionMappingBuilder +{ + public ExceptionMappingBuilder(IServiceCollection services) + { + Services = services ?? throw new ArgumentNullException(nameof(services)); + } + + public IServiceCollection Services { get; } + public IList Handlers { get; } = new List(); +} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper/ExceptionHandler.cs b/src/ForEvolve.ExceptionMapper/ExceptionHandler.cs new file mode 100644 index 0000000..cec492f --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/ExceptionHandler.cs @@ -0,0 +1,24 @@ +namespace ForEvolve.ExceptionMapper; + +public abstract class ExceptionHandler : IExceptionHandler + where TException : Exception +{ + public abstract int StatusCode { get; } + + public virtual Task CanHandle(Exception exception) + { + return Task.FromResult(exception is TException); + } + + public virtual Task ExecuteAsync(ExceptionHandlingContext context) + { + context.HttpContext.Response.StatusCode = StatusCode; + context.Result = new ExceptionHandledResult(context.Error); + return ExecuteCoreAsync(new ExceptionHandlingContext(context)); + } + + protected virtual Task ExecuteCoreAsync(ExceptionHandlingContext context) + { + return Task.CompletedTask; + } +} diff --git a/src/ForEvolve.ExceptionMapper/ExceptionHandlerCollection.cs b/src/ForEvolve.ExceptionMapper/ExceptionHandlerCollection.cs new file mode 100644 index 0000000..3dfc977 --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/ExceptionHandlerCollection.cs @@ -0,0 +1,69 @@ +using ForEvolve.ExceptionMapper; +using System.Collections; + +namespace Microsoft.Extensions.DependencyInjection; + +public class ExceptionHandlerCollection : IList +{ + private List _exceptionHandlers = new(); + public ExceptionHandlerCollection(IEnumerable exceptionHandlers) + { + _exceptionHandlers.AddRange(exceptionHandlers); + } + + public IExceptionHandler this[int index] { get => ((IList)_exceptionHandlers)[index]; set => ((IList)_exceptionHandlers)[index] = value; } + + public int Count => ((ICollection)_exceptionHandlers).Count; + + public bool IsReadOnly => ((ICollection)_exceptionHandlers).IsReadOnly; + + public void Add(IExceptionHandler item) + { + ((ICollection)_exceptionHandlers).Add(item); + } + + public void Clear() + { + ((ICollection)_exceptionHandlers).Clear(); + } + + public bool Contains(IExceptionHandler item) + { + return ((ICollection)_exceptionHandlers).Contains(item); + } + + public void CopyTo(IExceptionHandler[] array, int arrayIndex) + { + ((ICollection)_exceptionHandlers).CopyTo(array, arrayIndex); + } + + public IEnumerator GetEnumerator() + { + return ((IEnumerable)_exceptionHandlers).GetEnumerator(); + } + + public int IndexOf(IExceptionHandler item) + { + return ((IList)_exceptionHandlers).IndexOf(item); + } + + public void Insert(int index, IExceptionHandler item) + { + ((IList)_exceptionHandlers).Insert(index, item); + } + + public bool Remove(IExceptionHandler item) + { + return ((ICollection)_exceptionHandlers).Remove(item); + } + + public void RemoveAt(int index) + { + ((IList)_exceptionHandlers).RemoveAt(index); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_exceptionHandlers).GetEnumerator(); + } +} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper/ExceptionHandlingManager.cs b/src/ForEvolve.ExceptionMapper/ExceptionHandlingManager.cs new file mode 100644 index 0000000..9ce2a97 --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/ExceptionHandlingManager.cs @@ -0,0 +1,84 @@ +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using System.Collections.ObjectModel; +namespace ForEvolve.ExceptionMapper; + +public class ExceptionHandlingManager : IExceptionHandlingManager +{ + private readonly ExceptionMapperOptions _options; + + public ExceptionHandlingManager(ExceptionMapperOptions options) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + public IReadOnlyCollection Handlers + => new ReadOnlyCollection(_options.Handlers); + + public async Task HandleAsync(HttpContext httpContext) + { + var exceptionHandlerPathFeature = httpContext.Features.Get(); + if (exceptionHandlerPathFeature == null) + { + return new ExceptionHandlerFeatureNotSupportedResult(); + } + + var exception = exceptionHandlerPathFeature.Error; + if (exception == null) + { + return new NoExceptionResult(); + } + + var context = new ExceptionHandlingContext(httpContext, exception, new ExceptionNotHandledResult(exception)); + foreach (var handler in _options.Handlers) + { + if (await handler.CanHandle(exception)) + { + await handler.ExecuteAsync(context); + } + } + + await _options.Serializer.ExecuteAsync(context); + + return context.Result; + } +} + +public class ExceptionHandlingContext + where TException : Exception +{ + public ExceptionHandlingContext(ExceptionHandlingContext previous) + : this(previous.HttpContext, previous.Error as TException, previous.Result) + { + } + + public ExceptionHandlingContext(ExceptionHandlingContext previous) + : this(previous.HttpContext, previous.Error, previous.Result) + { + } + + public ExceptionHandlingContext(HttpContext httpContext, TException? error, IExceptionHandlingResult initialResult) + { + HttpContext = httpContext ?? throw new ArgumentNullException(nameof(httpContext)); + Error = error ?? throw new ArgumentNullException(nameof(error)); + Result = initialResult; + } + + public HttpContext HttpContext { get; } + public TException Error { get; } + public IExceptionHandlingResult Result { get; set; } +} + +public class ExceptionHandlingContext : ExceptionHandlingContext +{ + public ExceptionHandlingContext(ExceptionHandlingContext previous) + : base(previous) + { + } + + public ExceptionHandlingContext(HttpContext httpContext, Exception error, IExceptionHandlingResult initialResult) + : base(httpContext, error, initialResult) + { + } +} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper/ExceptionMapperOptions.cs b/src/ForEvolve.ExceptionMapper/ExceptionMapperOptions.cs new file mode 100644 index 0000000..cfbf8f4 --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/ExceptionMapperOptions.cs @@ -0,0 +1,15 @@ +using ForEvolve.ExceptionMapper; + +namespace Microsoft.Extensions.DependencyInjection; + +public class ExceptionMapperOptions +{ + public ExceptionMapperOptions(ExceptionHandlerCollection handlers, IExceptionSerializer serializer) + { + Handlers = handlers ?? throw new ArgumentNullException(nameof(handlers)); + Serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); + } + + public ExceptionHandlerCollection Handlers { get; } + public IExceptionSerializer Serializer { get; } +} diff --git a/src/ForEvolve.ExceptionMapper/ForEvolve.ExceptionMapper.csproj b/src/ForEvolve.ExceptionMapper/ForEvolve.ExceptionMapper.csproj index 89bdeca..23b5384 100644 --- a/src/ForEvolve.ExceptionMapper/ForEvolve.ExceptionMapper.csproj +++ b/src/ForEvolve.ExceptionMapper/ForEvolve.ExceptionMapper.csproj @@ -1,20 +1,11 @@  - - netstandard2.0;net5.0;net6.0 - - - - - - - - - - - - - - - + + $(FETargetFrameworks) + + + + + + diff --git a/src/ForEvolve.ExceptionMapper/HttpMiddleware/HttpExceptionHandlingMiddleware.cs b/src/ForEvolve.ExceptionMapper/HttpMiddleware/HttpExceptionHandlingMiddleware.cs new file mode 100644 index 0000000..c3f205e --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/HttpMiddleware/HttpExceptionHandlingMiddleware.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Http; +namespace ForEvolve.ExceptionMapper; + +public class HttpExceptionHandlingMiddleware +{ + private readonly RequestDelegate _next; + private readonly IExceptionHandlingManager _exceptionHandlingManager; + + public HttpExceptionHandlingMiddleware(IExceptionHandlingManager exceptionHandlingManager, RequestDelegate next) + { + _exceptionHandlingManager = exceptionHandlingManager ?? throw new ArgumentNullException(nameof(exceptionHandlingManager)); + _next = next; + } + + public async Task InvokeAsync(HttpContext context) + { + var result = await _exceptionHandlingManager.HandleAsync(context); + if (result.ExceptionHandled) + { + return; + } + await _next(context); + } +} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper/HttpMiddleware/UnhandledStatusCodeException.cs b/src/ForEvolve.ExceptionMapper/HttpMiddleware/UnhandledStatusCodeException.cs new file mode 100644 index 0000000..fb7d6c8 --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/HttpMiddleware/UnhandledStatusCodeException.cs @@ -0,0 +1,7 @@ +namespace ForEvolve.ExceptionMapper; + +public class UnhandledStatusCodeException : Exception +{ + public UnhandledStatusCodeException() + : base($"An unhandled error occurred.") { } +} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper/HttpMiddleware/UnhandledStatusCodeMiddleware.cs b/src/ForEvolve.ExceptionMapper/HttpMiddleware/UnhandledStatusCodeMiddleware.cs new file mode 100644 index 0000000..6a90231 --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/HttpMiddleware/UnhandledStatusCodeMiddleware.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Http; +namespace ForEvolve.ExceptionMapper; + +public class UnhandledStatusCodeMiddleware +{ + private readonly RequestDelegate _next; + + public UnhandledStatusCodeMiddleware(RequestDelegate next) => _next = next; + + public async Task InvokeAsync(HttpContext context) + { + await _next(context); + + if (context.Response.HasStarted) + { + return; + } + + // Client errors (400–499) + // Server errors (500–599) + if (context.Response.StatusCode >= 400) + { + switch (context.Response.StatusCode) + { + case StatusCodes.Status400BadRequest: + throw new BadRequestException(); + case StatusCodes.Status401Unauthorized: + throw new UnauthorizedException(); + case StatusCodes.Status403Forbidden: + throw new ForbiddenException(); + case StatusCodes.Status404NotFound: + throw new ResourceNotFoundException(context); + case StatusCodes.Status409Conflict: + throw new ConflictException(); + case StatusCodes.Status500InternalServerError: + throw new InternalServerErrorException(new UnhandledStatusCodeException()); + case StatusCodes.Status501NotImplemented: + throw new NotImplementedException(); + } + } + } +} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper/IExceptionHandler.cs b/src/ForEvolve.ExceptionMapper/IExceptionHandler.cs new file mode 100644 index 0000000..631f272 --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/IExceptionHandler.cs @@ -0,0 +1,7 @@ +namespace ForEvolve.ExceptionMapper; + +public interface IExceptionHandler +{ + Task CanHandle(Exception exception); + Task ExecuteAsync(ExceptionHandlingContext context); +} diff --git a/src/ForEvolve.ExceptionMapper/IExceptionHandlingManager.cs b/src/ForEvolve.ExceptionMapper/IExceptionHandlingManager.cs new file mode 100644 index 0000000..f79ba3a --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/IExceptionHandlingManager.cs @@ -0,0 +1,7 @@ +using Microsoft.AspNetCore.Http; +namespace ForEvolve.ExceptionMapper; + +public interface IExceptionHandlingManager +{ + Task HandleAsync(HttpContext context); +} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper/IExceptionMappingBuilder.cs b/src/ForEvolve.ExceptionMapper/IExceptionMappingBuilder.cs new file mode 100644 index 0000000..4e508dd --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/IExceptionMappingBuilder.cs @@ -0,0 +1,7 @@ +namespace Microsoft.Extensions.DependencyInjection; + +public interface IExceptionMappingBuilder +{ + IServiceCollection Services { get; } + //IList Handlers { get; } +} diff --git a/src/ForEvolve.ExceptionMapper/IExceptionSerializer.cs b/src/ForEvolve.ExceptionMapper/IExceptionSerializer.cs new file mode 100644 index 0000000..cc20880 --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/IExceptionSerializer.cs @@ -0,0 +1,6 @@ +namespace ForEvolve.ExceptionMapper; + +public interface IExceptionSerializer +{ + Task ExecuteAsync(ExceptionHandlingContext context); +} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper/Results/ExceptionHandledResult.cs b/src/ForEvolve.ExceptionMapper/Results/ExceptionHandledResult.cs new file mode 100644 index 0000000..652e1bd --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/Results/ExceptionHandledResult.cs @@ -0,0 +1,13 @@ +namespace ForEvolve.ExceptionMapper; + +public class ExceptionHandledResult : IExceptionHandlingResult +{ + public ExceptionHandledResult(Exception error) + { + Error = error ?? throw new ArgumentNullException(nameof(error)); + ExceptionHandled = true; + } + + public bool ExceptionHandled { get; } + public Exception Error { get; } +} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper/Results/ExceptionHandlerFeatureNotSupportedResult.cs b/src/ForEvolve.ExceptionMapper/Results/ExceptionHandlerFeatureNotSupportedResult.cs new file mode 100644 index 0000000..6a4a0c2 --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/Results/ExceptionHandlerFeatureNotSupportedResult.cs @@ -0,0 +1,7 @@ +namespace ForEvolve.ExceptionMapper; + +public sealed class ExceptionHandlerFeatureNotSupportedResult : IExceptionHandlingResult +{ + public bool ExceptionHandled { get; } + public Exception? Error { get; } +} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper/Results/ExceptionNotHandledResult.cs b/src/ForEvolve.ExceptionMapper/Results/ExceptionNotHandledResult.cs new file mode 100644 index 0000000..d2d02c3 --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/Results/ExceptionNotHandledResult.cs @@ -0,0 +1,12 @@ +namespace ForEvolve.ExceptionMapper; + +public class ExceptionNotHandledResult : IExceptionHandlingResult +{ + public ExceptionNotHandledResult(Exception error) + { + Error = error ?? throw new ArgumentNullException(nameof(error)); + } + + public bool ExceptionHandled { get; } + public Exception Error { get; } +} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper/Results/IExceptionHandlingResult.cs b/src/ForEvolve.ExceptionMapper/Results/IExceptionHandlingResult.cs new file mode 100644 index 0000000..f1a8ae5 --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/Results/IExceptionHandlingResult.cs @@ -0,0 +1,7 @@ +namespace ForEvolve.ExceptionMapper; + +public interface IExceptionHandlingResult +{ + bool ExceptionHandled { get; } + Exception? Error { get; } +} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper/Results/NoExceptionResult.cs b/src/ForEvolve.ExceptionMapper/Results/NoExceptionResult.cs new file mode 100644 index 0000000..9ef8bfc --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/Results/NoExceptionResult.cs @@ -0,0 +1,7 @@ +namespace ForEvolve.ExceptionMapper; + +public class NoExceptionResult : IExceptionHandlingResult +{ + public bool ExceptionHandled { get; } + public Exception? Error { get; } +} \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper/Serialization/DefaultProblemDetailsFactory.cs b/src/ForEvolve.ExceptionMapper/Serialization/DefaultProblemDetailsFactory.cs new file mode 100644 index 0000000..b61f82f --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/Serialization/DefaultProblemDetailsFactory.cs @@ -0,0 +1,142 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Options; +using System.Diagnostics; + +namespace ForEvolve.ExceptionMapper.Serialization; +// Source: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs +// This is a workaround until (hopefully) the following issue get resolved: https://github.com/dotnet/aspnetcore/issues/49982 + +internal sealed class DefaultProblemDetailsFactory : ProblemDetailsFactory +{ + private readonly ApiBehaviorOptions _options; + private readonly Action? _configure; + + public DefaultProblemDetailsFactory( + IOptions options, + IOptions? problemDetailsOptions = null) + { + _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + _configure = problemDetailsOptions?.Value?.CustomizeProblemDetails; + } + + public override ProblemDetails CreateProblemDetails( + HttpContext httpContext, + int? statusCode = null, + string? title = null, + string? type = null, + string? detail = null, + string? instance = null) + { + statusCode ??= 500; + + var problemDetails = new ProblemDetails + { + Status = statusCode, + Title = title, + Type = type, + Detail = detail, + Instance = instance, + }; + + ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value); + + return problemDetails; + } + + public override ValidationProblemDetails CreateValidationProblemDetails( + HttpContext httpContext, + ModelStateDictionary modelStateDictionary, + int? statusCode = null, + string? title = null, + string? type = null, + string? detail = null, + string? instance = null) + { + ArgumentNullException.ThrowIfNull(modelStateDictionary); + + statusCode ??= 400; + + var problemDetails = new ValidationProblemDetails(modelStateDictionary) + { + Status = statusCode, + Type = type, + Detail = detail, + Instance = instance, + }; + + if (title != null) + { + // For validation problem details, don't overwrite the default title with null. + problemDetails.Title = title; + } + + ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value); + + return problemDetails; + } + + private void ApplyProblemDetailsDefaults(HttpContext httpContext, ProblemDetails problemDetails, int statusCode) + { + problemDetails.Status ??= statusCode; + + if (_options.ClientErrorMapping.TryGetValue(statusCode, out var clientErrorData)) + { + problemDetails.Title ??= clientErrorData.Title; + problemDetails.Type ??= clientErrorData.Link; + } + + var traceId = Activity.Current?.Id ?? httpContext?.TraceIdentifier; + if (traceId != null) + { + problemDetails.Extensions["traceId"] = traceId; + } + + _configure?.Invoke(new() { HttpContext = httpContext!, ProblemDetails = problemDetails }); + } +} + +#if NET6_0 +public class ProblemDetailsOptions +{ + /// + /// The operation that customizes the current instance. + /// + public Action? CustomizeProblemDetails { get; set; } +} + +public class ProblemDetailsContext +{ + private ProblemDetails? _problemDetails; + + /// + /// The associated with the current request being processed by the filter. + /// + public HttpContext? HttpContext { get; init; } + + /// + /// A collection of additional arbitrary metadata associated with the current request endpoint. + /// + public EndpointMetadataCollection? AdditionalMetadata { get; init; } + + /// + /// An instance of that will be + /// used during the response payload generation. + /// + public ProblemDetails ProblemDetails + { + get => _problemDetails ??= new ProblemDetails(); + set => _problemDetails = value; + } + + /// + /// The exception causing the problem or null if no exception information is available. + /// + public Exception? Exception { get; init; } +} +#endif \ No newline at end of file diff --git a/src/ForEvolve.ExceptionMapper/Serialization/Json/ProblemDetailsSerializationHandler.cs b/src/ForEvolve.ExceptionMapper/Serialization/Json/ProblemDetailsSerializationHandler.cs new file mode 100644 index 0000000..87dcd6c --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/Serialization/Json/ProblemDetailsSerializationHandler.cs @@ -0,0 +1,146 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Json; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using System.ComponentModel; +using System.Diagnostics; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ForEvolve.ExceptionMapper.Serialization.Json; + +public class ProblemDetailsSerializationHandler : IExceptionSerializer +{ + private readonly ProblemDetailsFactory _problemDetailsFactory; + private readonly IHostEnvironment _hostEnvironment; + private readonly ProblemDetailsSerializationOptions _options; +#if NET7_0_OR_GREATER + private readonly IProblemDetailsService _problemDetailsService; +#endif + private readonly JsonSerializerOptions _jsonSerializerOptions; + + public ProblemDetailsSerializationHandler( +#if NET7_0_OR_GREATER + IProblemDetailsService problemDetailsService, +#endif + ProblemDetailsFactory problemDetailsFactory, + IHostEnvironment hostEnvironment, + ProblemDetailsSerializationOptions options, + IOptions jsonOptions) + { + _problemDetailsFactory = problemDetailsFactory ?? throw new ArgumentNullException(nameof(problemDetailsFactory)); + _hostEnvironment = hostEnvironment ?? throw new ArgumentNullException(nameof(hostEnvironment)); + _options = options ?? throw new ArgumentNullException(nameof(options)); +#if NET7_0_OR_GREATER + _problemDetailsService = problemDetailsService ?? throw new ArgumentNullException(nameof(problemDetailsService)); +#endif + _jsonSerializerOptions = jsonOptions.Value.SerializerOptions ?? throw new ArgumentNullException(nameof(jsonOptions)); + } + + public async Task ExecuteAsync(ExceptionHandlingContext ctx) + { + if (!_options.SerializeExceptions) + { + await ctx.HttpContext.Response.CompleteAsync(); + return; + } + + var problemDetails = _problemDetailsFactory.CreateProblemDetails( + ctx.HttpContext, + title: ctx.Error.Message, + statusCode: ctx.HttpContext.Response.StatusCode + ); + + // Add debug info + var displayDebugInformation = _options.DisplayDebugInformation?.Invoke(ctx) ?? false; + if (displayDebugInformation || _hostEnvironment.IsDevelopment()) + { + var errorType = ctx.Error.GetType(); + problemDetails.Extensions.Add( + FormatName("debug"), + new + { + type = new + { + name = errorType.Name, + fullName = errorType.FullName, + }, + stackTrace = ctx.Error.StackTrace, + } + ); + } + + // Remove the default "traceId" property and + // add it back with a key that is in line with the JSON serializer options. + var traceId = Activity.Current?.Id ?? ctx.HttpContext.TraceIdentifier; + if (traceId != null) + { + var traceIdKey = "traceId"; + problemDetails.Extensions.Remove(traceIdKey); + problemDetails.Extensions.Add(FormatName(traceIdKey), traceId); + } + + // Transfer non-excluded and non-JsonIgnored properties to the problem details. + var properties = TypeDescriptor.GetProperties(ctx.Error); + var propertiesToExclude = new string[] { + nameof(Exception.StackTrace), + nameof(Exception.Data), + nameof(Exception.HResult), + nameof(Exception.TargetSite), + nameof(Exception.Message), + nameof(Exception.Source), + nameof(Exception.InnerException), + }; + foreach (PropertyDescriptor property in properties) + { + if (propertiesToExclude.Contains(property.Name)) + { + continue; + } + if (property.Attributes.OfType().Any()) + { + continue; + } + + var value = property.GetValue(ctx.Error); + if (value != null) + { + problemDetails.Extensions.Add(FormatName(property.Name), value); + } + } + + // Output the problem details +#if NET7_0_OR_GREATER + var problemDetailsContext = new ProblemDetailsContext + { + HttpContext = ctx.HttpContext, +#if NET8_0_OR_GREATER + Exception = ctx.Error, +#endif + ProblemDetails = problemDetails, + }; + await _problemDetailsService.WriteAsync(problemDetailsContext); +#else +#pragma warning disable CS0618 // Type or member is obsolete + ctx.HttpContext.Response.ContentType = _options.ContentType; + await JsonSerializer.SerializeAsync( + ctx.HttpContext.Response.Body, + problemDetails, + _jsonSerializerOptions, + cancellationToken: ctx.HttpContext.RequestAborted + ); +#pragma warning restore CS0618 // Type or member is obsolete +#endif + } + + private string FormatName(string name) + { + return _jsonSerializerOptions.PropertyNamingPolicy?.ConvertName(name) ?? FormatToCamelCase(name); + + static string FormatToCamelCase(string name) + => string.Concat(name[0].ToString().ToLower(), name.AsSpan(1)); + } +} diff --git a/src/ForEvolve.ExceptionMapper/Serialization/Json/ProblemDetailsSerializationOptions.cs b/src/ForEvolve.ExceptionMapper/Serialization/Json/ProblemDetailsSerializationOptions.cs new file mode 100644 index 0000000..b9bbd10 --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/Serialization/Json/ProblemDetailsSerializationOptions.cs @@ -0,0 +1,15 @@ +using System.Text.Json; + +namespace ForEvolve.ExceptionMapper.Serialization.Json; + +public class ProblemDetailsSerializationOptions +{ + public bool SerializeExceptions { get; set; } = true; + public Func DisplayDebugInformation { get; set; } = (ExceptionHandlingContext ctx) => false; +#if NET6_0 + private const string _obsoleteMessage = "This property was removed when targeting .NET 7+. The library now leverages the `IProblemDetailsService` interface instead."; + + [Obsolete(_obsoleteMessage)] + public string ContentType { get; set; } = "application/problem+json; charset=utf-8"; +#endif +} diff --git a/src/ForEvolve.ExceptionMapper/StatusCodeExceptionHandler.cs b/src/ForEvolve.ExceptionMapper/StatusCodeExceptionHandler.cs new file mode 100644 index 0000000..4515322 --- /dev/null +++ b/src/ForEvolve.ExceptionMapper/StatusCodeExceptionHandler.cs @@ -0,0 +1,11 @@ +namespace ForEvolve.ExceptionMapper; + +public class StatusCodeExceptionHandler : ExceptionHandler + where TException : Exception +{ + public StatusCodeExceptionHandler(int statusCode) + { + StatusCode = statusCode; + } + public override int StatusCode { get; } +} \ No newline at end of file diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 3aae19f..4459169 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -6,10 +6,10 @@ - - - - - + + + + +
\ No newline at end of file diff --git a/test/ForEvolve.ExceptionMapper.CommonExceptions.Tests/ForEvolve.ExceptionMapper.CommonExceptions.Tests.csproj b/test/ForEvolve.ExceptionMapper.CommonExceptions.Tests/ForEvolve.ExceptionMapper.CommonExceptions.Tests.csproj deleted file mode 100644 index a9269f0..0000000 --- a/test/ForEvolve.ExceptionMapper.CommonExceptions.Tests/ForEvolve.ExceptionMapper.CommonExceptions.Tests.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - ForEvolve.ExceptionMapper.CommonExceptions - - - ForEvolve.ExceptionMapper - - - - net5.0;net6.0 - false - - - - - - - diff --git a/test/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests/Fallback/FallbackExceptionHandlerTest.cs b/test/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests/Fallback/FallbackExceptionHandlerTest.cs deleted file mode 100644 index fa2dfdc..0000000 --- a/test/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests/Fallback/FallbackExceptionHandlerTest.cs +++ /dev/null @@ -1,87 +0,0 @@ -using ForEvolve.Testing.AspNetCore.Http; -using Microsoft.Extensions.Options; -using Moq; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit; - -namespace ForEvolve.ExceptionMapper.Handlers.Fallback -{ - public class FallbackExceptionHandlerTest - { - private readonly FallbackExceptionHandlerOptions _options; - private readonly Mock> _optionsMonitorMock; - private readonly FallbackExceptionHandler sut; - - public FallbackExceptionHandlerTest() - { - _options = new FallbackExceptionHandlerOptions(); - _optionsMonitorMock = new Mock>(); - _optionsMonitorMock.Setup(x => x.CurrentValue).Returns(_options); - sut = new FallbackExceptionHandler(_optionsMonitorMock.Object); - } - - public class Order : FallbackExceptionHandlerTest - { - [Fact] - public void Should_be_equal_to_FallbackOrder() - { - Assert.Equal(HandlerOrder.FallbackOrder, sut.Order); - } - } - - public class KnowHowToHandleAsync : FallbackExceptionHandlerTest - { - [Fact] - public async Task Should_return_true_when_FallbackStrategy_equals_Handle() - { - _options.Strategy = FallbackStrategy.Handle; - var result = await sut.KnowHowToHandleAsync(new Exception()); - Assert.True(result); - } - - [Fact] - public async Task Should_return_false_when_FallbackStrategy_equals_Ignore() - { - _options.Strategy = FallbackStrategy.Ignore; - var result = await sut.KnowHowToHandleAsync(new Exception()); - Assert.False(result); - } - } - - public class ExecuteAsync : FallbackExceptionHandlerTest - { - private readonly Exception _error; - private readonly ExceptionHandlingContext _context; - private readonly HttpContextHelper _httpContextHelper; - private readonly ExceptionNotHandledResult _initialResult; - - public ExecuteAsync() - { - _error = new Exception(); - _initialResult = new ExceptionNotHandledResult(_error); - _httpContextHelper = new HttpContextHelper(); - _context = new ExceptionHandlingContext(_httpContextHelper.HttpContext, _error, _initialResult); - } - - [Fact] - public async Task Should_handle_the_exception_when_FallbackStrategy_equals_Handle() - { - _options.Strategy = FallbackStrategy.Handle; - await sut.ExecuteAsync(_context); - Assert.IsType(_context.Result); - } - - [Fact] - public async Task Should_do_nothing_when_FallbackStrategy_equals_Ignore() - { - _options.Strategy = FallbackStrategy.Ignore; - await sut.ExecuteAsync(_context); - Assert.Same(_initialResult, _context.Result); - } - } - } -} diff --git a/test/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests.csproj b/test/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests.csproj deleted file mode 100644 index 2dad928..0000000 --- a/test/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers - - - - net5.0;net6.0 - false - - - - - - - diff --git a/test/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests/Handlers/ConflictExceptionHandlerTest.cs b/test/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests/Handlers/ConflictExceptionHandlerTest.cs deleted file mode 100644 index 520a967..0000000 --- a/test/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests/Handlers/ConflictExceptionHandlerTest.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.AspNetCore.Http; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit; - -namespace ForEvolve.ExceptionMapper.Handlers -{ - public class ConflictExceptionHandlerTest - { - [Fact] - public void StatusCode_should_equal_409() - { - var sut = new ConflictExceptionHandler(); - Assert.Equal(StatusCodes.Status409Conflict, sut.StatusCode); - } - } -} diff --git a/test/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests/Handlers/InternalServerErrorExceptionHandlerTest.cs b/test/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests/Handlers/InternalServerErrorExceptionHandlerTest.cs deleted file mode 100644 index dbd1fba..0000000 --- a/test/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests/Handlers/InternalServerErrorExceptionHandlerTest.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Xunit; - -namespace ForEvolve.ExceptionMapper.Handlers -{ - public class InternalServerErrorExceptionHandlerTest - { - [Fact] - public void StatusCode_should_equal_500() - { - var sut = new InternalServerErrorExceptionHandler(); - Assert.Equal(StatusCodes.Status500InternalServerError, sut.StatusCode); - } - } -} diff --git a/test/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests/Handlers/NotFoundExceptionHandlerTest.cs b/test/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests/Handlers/NotFoundExceptionHandlerTest.cs deleted file mode 100644 index 65d767e..0000000 --- a/test/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests/Handlers/NotFoundExceptionHandlerTest.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Xunit; - -namespace ForEvolve.ExceptionMapper.Handlers -{ - public class NotFoundExceptionHandlerTest - { - [Fact] - public void StatusCode_should_equal_404() - { - var sut = new NotFoundExceptionHandler(); - Assert.Equal(StatusCodes.Status404NotFound, sut.StatusCode); - } - } -} diff --git a/test/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests/Handlers/NotImplementedExceptionHandlerTest.cs b/test/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests/Handlers/NotImplementedExceptionHandlerTest.cs deleted file mode 100644 index c2b125e..0000000 --- a/test/ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers.Tests/Handlers/NotImplementedExceptionHandlerTest.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Xunit; - -namespace ForEvolve.ExceptionMapper.Handlers -{ - public class NotImplementedExceptionHandlerTest - { - [Fact] - public void StatusCode_should_equal_501() - { - var sut = new NotImplementedExceptionHandler(); - Assert.Equal(StatusCodes.Status501NotImplemented, sut.StatusCode); - } - } -} diff --git a/test/ForEvolve.ExceptionMapper.Core.Tests/ExceptionHandlerTest.cs b/test/ForEvolve.ExceptionMapper.Core.Tests/ExceptionHandlerTest.cs deleted file mode 100644 index 54dc971..0000000 --- a/test/ForEvolve.ExceptionMapper.Core.Tests/ExceptionHandlerTest.cs +++ /dev/null @@ -1,113 +0,0 @@ -using ForEvolve.Testing.AspNetCore.Http; -using Microsoft.AspNetCore.Http; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit; - -namespace ForEvolve.ExceptionMapper -{ - public class ExceptionHandlerTest - { - public class KnowHowToHandleAsync : ExceptionHandlerTest - { - public static TheoryData TrueResults = new TheoryData - { - new TestException(), - new TestSubException() - }; - - public static TheoryData FalseResults = new TheoryData - { - new TestWrongException(), - new Exception() - }; - - [Theory] - [MemberData(nameof(TrueResults))] - public async Task Should_return_true_when_the_exception_is_a_TException(TestException exception) - { - var sut = new ExceptionTestHandler(); - var result = await sut.KnowHowToHandleAsync(exception); - Assert.True(result); - } - - [Theory] - [MemberData(nameof(FalseResults))] - public async Task Should_return_false_when_the_exception_type_is_not_a_TException(Exception exception) - { - var sut = new ExceptionTestHandler(); - var result = await sut.KnowHowToHandleAsync(exception); - Assert.False(result); - } - - } - - public class ExecuteAsync : ExceptionHandlerTest - { - private HttpContextHelper _httpContextHelper = new HttpContextHelper(); - - [Fact] - public async Task Should_set_response_StatusCode_to_handler_StatusCode_value() - { - var sut = new ExceptionTestHandler(); - var exception = new TestException(); - - await sut.ExecuteAsync(new ExceptionHandlingContext( - _httpContextHelper.HttpContextMock.Object, - exception, - default - )); - - Assert.Equal( - sut.StatusCode, - _httpContextHelper.HttpResponse.StatusCode - ); - } - - [Fact] - public async Task Should_call_ExecuteCoreAsync() - { - var sut = new ExceptionTestHandler(); - var exception = new TestException(); - - await sut.ExecuteAsync(new ExceptionHandlingContext( - _httpContextHelper.HttpContextMock.Object, - exception, - default - )); - - Assert.True(sut.HandleCoreWasCalled); - Assert.Same(exception, sut.Exception); - Assert.Same( - _httpContextHelper.HttpContextMock.Object, - sut.HttpContext - ); - } - } - - private class ExceptionTestHandler : ExceptionHandler - where TException : Exception - { - public override int StatusCode => 999; - - protected override Task ExecuteCoreAsync(ExceptionHandlingContext context) - { - HandleCoreWasCalled = true; - HttpContext = context.HttpContext; - Exception = context.Error; - return base.ExecuteCoreAsync(context); - } - - public bool HandleCoreWasCalled { get; private set; } - public TException Exception { get; private set; } - public HttpContext HttpContext { get; private set; } - } - - public class TestException : Exception { } - public class TestSubException : TestException { } - public class TestWrongException : Exception { } - } -} diff --git a/test/ForEvolve.ExceptionMapper.Core.Tests/ExceptionHandlingManagerTest.cs b/test/ForEvolve.ExceptionMapper.Core.Tests/ExceptionHandlingManagerTest.cs deleted file mode 100644 index 54e1103..0000000 --- a/test/ForEvolve.ExceptionMapper.Core.Tests/ExceptionHandlingManagerTest.cs +++ /dev/null @@ -1,227 +0,0 @@ -using ForEvolve.Testing.AspNetCore.Http; -using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.Http; -using Moq; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit; -using Xunit.Sdk; - -namespace ForEvolve.ExceptionMapper -{ - public class ExceptionHandlingManagerTest - { - public class HandleAsync : ExceptionHandlingManagerTest - { - private readonly HttpContextHelper _httpContextHelper = new HttpContextHelper(); - private HttpContext HttpContext => _httpContextHelper.HttpContextMock.Object; - private readonly List _handlers = new List(); - - public class When_IExceptionHandlerFeature_is_null : HandleAsync - { - [Fact] - public async Task Should_return_ExceptionHandlerFeatureNotSupportedResult() - { - // Arrange - var sut = new ExceptionHandlingManager(_handlers); - _httpContextHelper.FeaturesMock - .Setup(x => x.Get()) - .Returns(default(IExceptionHandlerFeature)); - - // Act - var result = await sut.HandleAsync(HttpContext); - - // Assert - Assert.IsType(result); - } - } - - public class When_IExceptionHandlerFeature_is_not_null : HandleAsync - { - private readonly Mock _exceptionHandlerFeatureMock; - - public When_IExceptionHandlerFeature_is_not_null() - { - _exceptionHandlerFeatureMock = new Mock(); - _httpContextHelper.FeaturesMock - .Setup(x => x.Get()) - .Returns(_exceptionHandlerFeatureMock.Object); - } - - public class And_has_no_Error : When_IExceptionHandlerFeature_is_not_null - { - [Fact] - public async Task Should_return_NoExceptionResult() - { - // Arrange - _exceptionHandlerFeatureMock - .Setup(x => x.Error) - .Returns(default(Exception)); - var sut = new ExceptionHandlingManager(_handlers); - - // Act - var result = await sut.HandleAsync(HttpContext); - - // Assert - Assert.IsType(result); - } - } - - public class And_has_an_Error : When_IExceptionHandlerFeature_is_not_null - { - private readonly Exception _exception; - public And_has_an_Error() - { - _exception = new Exception(); - _exceptionHandlerFeatureMock - .Setup(x => x.Error) - .Returns(_exception); - } - - public class And_the_Exception_was_handled : And_has_an_Error - { - [Fact] - public async Task Should_return_ExceptionHandlingContext_Result() - { - // Arrange - var handlerMock = new Mock(); - handlerMock - .Setup(x => x.KnowHowToHandleAsync(_exception)) - .ReturnsAsync(true); - handlerMock - .Setup(x => x.ExecuteAsync(It.IsAny())) - .Callback((ExceptionHandlingContext context) => context.Result = new TestResult()) - .Returns(Task.CompletedTask); - _handlers.Add(handlerMock.Object); - var sut = new ExceptionHandlingManager(_handlers); - - // Act - var result = await sut.HandleAsync(HttpContext); - - // Assert - Assert.IsType(result); - } - - private class TestResult : IExceptionHandlingResult - { - public bool ExceptionHandled => throw new NotImplementedException(); - - public Exception Error => throw new NotImplementedException(); - - public bool ExceptionHandlerFeatureSupported => throw new NotImplementedException(); - } - } - - public class And_the_Exception_was_not_handled_by_any_handler : And_has_an_Error - { - [Fact] - public async Task Should_return_ExceptionNotHandledResult() - { - // Arrange - var sut = new ExceptionHandlingManager(_handlers); - - // Act - var result = await sut.HandleAsync(HttpContext); - - // Assert - Assert.IsType(result); - } - } - } - } - } - - public class Handlers : ExceptionHandlingManagerTest - { - private static readonly OrderableTestExceptionHandler _handlerOrder1 = new OrderableTestExceptionHandler { Order = 1 }; - private static readonly OrderableTestExceptionHandler _handlerOrder2 = new OrderableTestExceptionHandler { Order = 2 }; - private static readonly OrderableTestExceptionHandler _handlerOrder3 = new OrderableTestExceptionHandler { Order = 3 }; - - private static readonly OrderableTestExceptionHandler _handlerOrder1Version1 = new OrderableTestExceptionHandler { Order = 1 }; - private static readonly OrderableTestExceptionHandler _handlerOrder1Version2 = new OrderableTestExceptionHandler { Order = 1 }; - private static readonly OrderableTestExceptionHandler _handlerOrder1Version3 = new OrderableTestExceptionHandler { Order = 1 }; - - public static TheoryData, Action>> OrderTestsData = new TheoryData, Action>> - { - { - "Should sort handlers using their Order property", - new[] { - _handlerOrder2, - _handlerOrder1, - _handlerOrder3, - }, - orderedHandlers => Assert.Collection(orderedHandlers, - handler => Assert.Same(_handlerOrder1, handler), - handler => Assert.Same(_handlerOrder2, handler), - handler => Assert.Same(_handlerOrder3, handler) - ) - }, - { - "Should sort handlers 'first in first out'", - new[] { - _handlerOrder1Version1, - _handlerOrder2, - _handlerOrder1, - _handlerOrder3, - _handlerOrder1Version2, - _handlerOrder1Version3 - }, - orderedHandlers => Assert.Collection(orderedHandlers, - handler => Assert.Same(_handlerOrder1Version1, handler), - handler => Assert.Same(_handlerOrder1, handler), - handler => Assert.Same(_handlerOrder1Version2, handler), - handler => Assert.Same(_handlerOrder1Version3, handler), - handler => Assert.Same(_handlerOrder2, handler), - handler => Assert.Same(_handlerOrder3, handler) - ) - } - }; - - [Theory] - [MemberData(nameof(OrderTestsData))] - public void Should_order_handlers( - string errorMessage, - IEnumerable input, - Action> assert - ) - { - var sut = new ExceptionHandlingManager(input); - var orderedHandlers = sut.Handlers; - try - { - assert(orderedHandlers); - } - catch (XunitException ex) - { - throw new DescriptiveException(errorMessage, ex); - } - } - - public class OrderableTestExceptionHandler : IExceptionHandler - { - public int Order { get; set; } - - public Task ExecuteAsync(ExceptionHandlingContext context) - { - throw new NotImplementedException(); - } - - public Task KnowHowToHandleAsync(Exception exception) - { - throw new NotImplementedException(); - } - } - - public class DescriptiveException : XunitException - { - public DescriptiveException(string userMessage, XunitException innerException) - : base(userMessage, innerException) - { - } - } - } - } -} diff --git a/test/ForEvolve.ExceptionMapper.Core.Tests/ForEvolve.ExceptionMapper.Core.Tests.csproj b/test/ForEvolve.ExceptionMapper.Core.Tests/ForEvolve.ExceptionMapper.Core.Tests.csproj deleted file mode 100644 index b00e988..0000000 --- a/test/ForEvolve.ExceptionMapper.Core.Tests/ForEvolve.ExceptionMapper.Core.Tests.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - ForEvolve.ExceptionMapper.Core - - - - net5.0;net6.0 - false - - - - - - - diff --git a/test/ForEvolve.ExceptionMapper.Core.Tests/Results/ExceptionHandledResultTest.cs b/test/ForEvolve.ExceptionMapper.Core.Tests/Results/ExceptionHandledResultTest.cs deleted file mode 100644 index 2d42735..0000000 --- a/test/ForEvolve.ExceptionMapper.Core.Tests/Results/ExceptionHandledResultTest.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit; - -namespace ForEvolve.ExceptionMapper -{ - public class ExceptionHandledResultTest - { - [Fact] - public void Should_set_the_expected_values() - { - // Arrange - var exception = new Exception(); - - // Act - var result = new ExceptionHandledResult(exception); - - // Assert - Assert.Equal(exception, result.Error); - Assert.True(result.ExceptionHandled); - Assert.True(result.ExceptionHandlerFeatureSupported); - } - - [Fact] - public void Should_guard_against_null() - { - Assert.Throws( - "error", - () => new ExceptionHandledResult(default) - ); - } - } -} diff --git a/test/ForEvolve.ExceptionMapper.Core.Tests/Results/ExceptionHandlerFeatureNotSupportedResultTest.cs b/test/ForEvolve.ExceptionMapper.Core.Tests/Results/ExceptionHandlerFeatureNotSupportedResultTest.cs deleted file mode 100644 index 65e8891..0000000 --- a/test/ForEvolve.ExceptionMapper.Core.Tests/Results/ExceptionHandlerFeatureNotSupportedResultTest.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Xunit; - -namespace ForEvolve.ExceptionMapper -{ - public class ExceptionHandlerFeatureNotSupportedResultTest - { - [Fact] - public void Should_set_the_expected_values() - { - // Act - var result = new ExceptionHandlerFeatureNotSupportedResult(); - - // Assert - Assert.Null(result.Error); - Assert.False(result.ExceptionHandled); - Assert.False(result.ExceptionHandlerFeatureSupported); - } - } -} diff --git a/test/ForEvolve.ExceptionMapper.Core.Tests/Results/ExceptionNotHandledResultTest.cs b/test/ForEvolve.ExceptionMapper.Core.Tests/Results/ExceptionNotHandledResultTest.cs deleted file mode 100644 index 43ac042..0000000 --- a/test/ForEvolve.ExceptionMapper.Core.Tests/Results/ExceptionNotHandledResultTest.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using Xunit; - -namespace ForEvolve.ExceptionMapper -{ - public class ExceptionNotHandledResultTest - { - [Fact] - public void Should_set_the_expected_values() - { - // Arrange - var exception = new Exception(); - - // Act - var result = new ExceptionNotHandledResult(exception); - - // Assert - Assert.Equal(exception, result.Error); - Assert.False(result.ExceptionHandled); - Assert.True(result.ExceptionHandlerFeatureSupported); - } - - [Fact] - public void Should_guard_against_null() - { - Assert.Throws( - "error", - () => new ExceptionNotHandledResult(default) - ); - } - } -} diff --git a/test/ForEvolve.ExceptionMapper.Core.Tests/Results/NoExceptionResultTest.cs b/test/ForEvolve.ExceptionMapper.Core.Tests/Results/NoExceptionResultTest.cs deleted file mode 100644 index c460d23..0000000 --- a/test/ForEvolve.ExceptionMapper.Core.Tests/Results/NoExceptionResultTest.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Xunit; - -namespace ForEvolve.ExceptionMapper -{ - public class NoExceptionResultTest - { - [Fact] - public void Should_set_the_expected_values() - { - // Act - var result = new NoExceptionResult(); - - // Assert - Assert.Null(result.Error); - Assert.False(result.ExceptionHandled); - Assert.True(result.ExceptionHandlerFeatureSupported); - } - } -} diff --git a/test/ForEvolve.ExceptionMapper.Core.Tests/ServiceCollectionExtensionsTest.cs b/test/ForEvolve.ExceptionMapper.Core.Tests/ServiceCollectionExtensionsTest.cs deleted file mode 100644 index e5383cd..0000000 --- a/test/ForEvolve.ExceptionMapper.Core.Tests/ServiceCollectionExtensionsTest.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit; - -namespace ForEvolve.ExceptionMapper -{ - public class ServiceCollectionExtensionsTest - { - [Fact] - public void Should_register_all_dependencies() - { - // Arrange - var services = new ServiceCollection(); - services.AddExceptionMapper(); - var serviceProvider = services.BuildServiceProvider(); - - // Act - var manager = serviceProvider - .GetRequiredService(); - - // Assert - Assert.NotNull(manager); - } - } -} diff --git a/test/ForEvolve.ExceptionMapper.FluentMapper.Tests/ForEvolve.ExceptionMapper.FluentMapper.Tests.csproj b/test/ForEvolve.ExceptionMapper.FluentMapper.Tests/ForEvolve.ExceptionMapper.FluentMapper.Tests.csproj deleted file mode 100644 index 50428f3..0000000 --- a/test/ForEvolve.ExceptionMapper.FluentMapper.Tests/ForEvolve.ExceptionMapper.FluentMapper.Tests.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - ForEvolve.ExceptionMapper.FluentMapper - - - - net5.0;net6.0 - false - - - - - - - diff --git a/test/ForEvolve.ExceptionMapper.FluentMapper.Tests/UnitTest1.cs b/test/ForEvolve.ExceptionMapper.FluentMapper.Tests/UnitTest1.cs deleted file mode 100644 index c73de6f..0000000 --- a/test/ForEvolve.ExceptionMapper.FluentMapper.Tests/UnitTest1.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using Xunit; - -namespace ForEvolve.ExceptionMapper.FluentMapper.Tests -{ - public class UnitTest1 - { - [Fact] - public void Test1() - { - - } - } -} diff --git a/test/ForEvolve.ExceptionMapper.HttpMiddleware.Tests/ForEvolve.ExceptionMapper.HttpMiddleware.Tests.csproj b/test/ForEvolve.ExceptionMapper.HttpMiddleware.Tests/ForEvolve.ExceptionMapper.HttpMiddleware.Tests.csproj deleted file mode 100644 index bf24c7f..0000000 --- a/test/ForEvolve.ExceptionMapper.HttpMiddleware.Tests/ForEvolve.ExceptionMapper.HttpMiddleware.Tests.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - ForEvolve.ExceptionMapper.HttpMiddleware - - - - net5.0;net6.0 - false - - - - - - - diff --git a/test/ForEvolve.ExceptionMapper.HttpMiddleware.Tests/HttpExceptionHandlingMiddlewareTest.cs b/test/ForEvolve.ExceptionMapper.HttpMiddleware.Tests/HttpExceptionHandlingMiddlewareTest.cs deleted file mode 100644 index 4803ab8..0000000 --- a/test/ForEvolve.ExceptionMapper.HttpMiddleware.Tests/HttpExceptionHandlingMiddlewareTest.cs +++ /dev/null @@ -1,72 +0,0 @@ -using ForEvolve.Testing.AspNetCore.Http; -using Microsoft.AspNetCore.Http; -using Moq; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit; - -namespace ForEvolve.ExceptionMapper -{ - public class HttpExceptionHandlingMiddlewareTest - { - private readonly HttpContextHelper _httpContextHelper = new HttpContextHelper(); - private HttpContext HttpContext => _httpContextHelper.HttpContextMock.Object; - private bool _nextWasCalled = false; - private readonly HttpExceptionHandlingMiddleware sut; - private readonly Mock _exceptionHandlingManagerMock; - - public HttpExceptionHandlingMiddlewareTest() - { - _exceptionHandlingManagerMock = new Mock(); - sut = new HttpExceptionHandlingMiddleware( - _exceptionHandlingManagerMock.Object, - Next - ); - } - - [Fact] - public async Task Should_not_call_next_when_exception_is_handled() - { - // Arrange - var resultMock = new Mock(); - resultMock.Setup(x => x.ExceptionHandled).Returns(true); - _exceptionHandlingManagerMock - .Setup(x => x.HandleAsync(It.IsAny())) - .ReturnsAsync(resultMock.Object) - ; - - // Act - await sut.InvokeAsync(HttpContext); - - // Assert - Assert.False(_nextWasCalled); - } - - [Fact] - public async Task Should_call_next_when_exception_is_not_handled() - { - // Arrange - var resultMock = new Mock(); - resultMock.Setup(x => x.ExceptionHandled).Returns(false); - _exceptionHandlingManagerMock - .Setup(x => x.HandleAsync(It.IsAny())) - .ReturnsAsync(resultMock.Object) - ; - - // Act - await sut.InvokeAsync(HttpContext); - - // Assert - Assert.True(_nextWasCalled); - } - - private Task Next(HttpContext context) - { - _nextWasCalled = true; - return Task.CompletedTask; - } - } -} diff --git a/test/ForEvolve.ExceptionMapper.Mvc.Tests/ForEvolve.ExceptionMapper.Mvc.Tests.csproj b/test/ForEvolve.ExceptionMapper.Mvc.Tests/ForEvolve.ExceptionMapper.Mvc.Tests.csproj deleted file mode 100644 index 1e41f9d..0000000 --- a/test/ForEvolve.ExceptionMapper.Mvc.Tests/ForEvolve.ExceptionMapper.Mvc.Tests.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - ForEvolve.ExceptionMapper.Mvc - - - - net5.0;net6.0 - false - - - - - - - diff --git a/test/ForEvolve.ExceptionMapper.Mvc.Tests/UnitTest1.cs b/test/ForEvolve.ExceptionMapper.Mvc.Tests/UnitTest1.cs deleted file mode 100644 index 138bab8..0000000 --- a/test/ForEvolve.ExceptionMapper.Mvc.Tests/UnitTest1.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using Xunit; - -namespace ForEvolve.ExceptionMapper.Mvc.Tests -{ - public class UnitTest1 - { - [Fact] - public void Test1() - { - - } - } -} diff --git a/test/ForEvolve.ExceptionMapper.Scrutor.Tests/ForEvolve.ExceptionMapper.Scrutor.Tests.csproj b/test/ForEvolve.ExceptionMapper.Scrutor.Tests/ForEvolve.ExceptionMapper.Scrutor.Tests.csproj deleted file mode 100644 index 175e3fa..0000000 --- a/test/ForEvolve.ExceptionMapper.Scrutor.Tests/ForEvolve.ExceptionMapper.Scrutor.Tests.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - ForEvolve.ExceptionMapper.Scrutor - - - - net5.0;net6.0 - false - - - - - - - diff --git a/test/ForEvolve.ExceptionMapper.Scrutor.Tests/UnitTest1.cs b/test/ForEvolve.ExceptionMapper.Scrutor.Tests/UnitTest1.cs deleted file mode 100644 index e588bdc..0000000 --- a/test/ForEvolve.ExceptionMapper.Scrutor.Tests/UnitTest1.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using Xunit; - -namespace ForEvolve.ExceptionMapper.Scrutor.Tests -{ - public class UnitTest1 - { - [Fact] - public void Test1() - { - - } - } -} diff --git a/test/ForEvolve.ExceptionMapper.Serialization.Json.Tests/ForEvolve.ExceptionMapper.Serialization.Json.Tests.csproj b/test/ForEvolve.ExceptionMapper.Serialization.Json.Tests/ForEvolve.ExceptionMapper.Serialization.Json.Tests.csproj deleted file mode 100644 index 65ec58a..0000000 --- a/test/ForEvolve.ExceptionMapper.Serialization.Json.Tests/ForEvolve.ExceptionMapper.Serialization.Json.Tests.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - ForEvolve.ExceptionMapper.Serialization.Json - - - - net5.0;net6.0 - false - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/test/ForEvolve.ExceptionMapper.Serialization.Json.Tests/UnitTest1.cs b/test/ForEvolve.ExceptionMapper.Serialization.Json.Tests/UnitTest1.cs deleted file mode 100644 index 1f7c332..0000000 --- a/test/ForEvolve.ExceptionMapper.Serialization.Json.Tests/UnitTest1.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using Xunit; - -namespace ForEvolve.ExceptionMapper.Serialization.Json.Tests -{ - public class UnitTest1 - { - [Fact] - public void Test1() - { - - } - } -} diff --git a/test/ForEvolve.ExceptionMapper.Tests/CommonHttpExceptionHandlers/Fallback/FallbackExceptionHandlerTest.cs b/test/ForEvolve.ExceptionMapper.Tests/CommonHttpExceptionHandlers/Fallback/FallbackExceptionHandlerTest.cs new file mode 100644 index 0000000..9288a3e --- /dev/null +++ b/test/ForEvolve.ExceptionMapper.Tests/CommonHttpExceptionHandlers/Fallback/FallbackExceptionHandlerTest.cs @@ -0,0 +1,74 @@ +using ForEvolve.Testing.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Moq; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace ForEvolve.ExceptionMapper.Handlers.Fallback; + +public class FallbackExceptionHandlerTest +{ + private readonly FallbackExceptionHandlerOptions _options; + private readonly Mock> _optionsMonitorMock; + private readonly FallbackExceptionHandler sut; + + public FallbackExceptionHandlerTest() + { + _options = new FallbackExceptionHandlerOptions(); + _optionsMonitorMock = new Mock>(); + _optionsMonitorMock.Setup(x => x.CurrentValue).Returns(_options); + sut = new FallbackExceptionHandler(_optionsMonitorMock.Object); + } + + public class KnowHowToHandleAsync : FallbackExceptionHandlerTest + { + [Fact] + public async Task Should_return_true_when_FallbackStrategy_equals_Handle() + { + _options.Strategy = FallbackStrategy.Handle; + var result = await sut.CanHandle(new Exception()); + Assert.True(result); + } + + [Fact] + public async Task Should_return_false_when_FallbackStrategy_equals_Ignore() + { + _options.Strategy = FallbackStrategy.Ignore; + var result = await sut.CanHandle(new Exception()); + Assert.False(result); + } + } + + public class ExecuteAsync : FallbackExceptionHandlerTest + { + private readonly Exception _error; + private readonly ExceptionHandlingContext _context; + private readonly HttpContextHelper _httpContextHelper; + private readonly ExceptionNotHandledResult _initialResult; + + public ExecuteAsync() + { + _error = new Exception(); + _initialResult = new ExceptionNotHandledResult(_error); + _httpContextHelper = new HttpContextHelper(); + _context = new ExceptionHandlingContext(_httpContextHelper.HttpContext, _error, _initialResult); + } + + [Fact] + public async Task Should_handle_the_exception_when_FallbackStrategy_equals_Handle() + { + _options.Strategy = FallbackStrategy.Handle; + await sut.ExecuteAsync(_context); + Assert.IsType(_context.Result); + } + + [Fact] + public async Task Should_do_nothing_when_FallbackStrategy_equals_Ignore() + { + _options.Strategy = FallbackStrategy.Ignore; + await sut.ExecuteAsync(_context); + Assert.Same(_initialResult, _context.Result); + } + } +} diff --git a/test/ForEvolve.ExceptionMapper.Tests/DependencyInjection/ServiceCollectionExtensionsTest.cs b/test/ForEvolve.ExceptionMapper.Tests/DependencyInjection/ServiceCollectionExtensionsTest.cs new file mode 100644 index 0000000..119a91d --- /dev/null +++ b/test/ForEvolve.ExceptionMapper.Tests/DependencyInjection/ServiceCollectionExtensionsTest.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Moq; +using Xunit; + +namespace ForEvolve.ExceptionMapper; + +public class ServiceCollectionExtensionsTest +{ + [Fact] + public void Should_register_all_dependencies() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var hostEnvironmentMock = new Mock(); + var services = new ServiceCollection(); + services.AddSingleton(hostEnvironmentMock.Object); + services.AddExceptionMapper(configuration); + var serviceProvider = services.BuildServiceProvider(); + + // Act + var manager = serviceProvider + .GetRequiredService(); + + // Assert + Assert.NotNull(manager); + } +} diff --git a/test/ForEvolve.ExceptionMapper.Tests/ExceptionHandlerTest.cs b/test/ForEvolve.ExceptionMapper.Tests/ExceptionHandlerTest.cs new file mode 100644 index 0000000..9c188ec --- /dev/null +++ b/test/ForEvolve.ExceptionMapper.Tests/ExceptionHandlerTest.cs @@ -0,0 +1,109 @@ +using ForEvolve.Testing.AspNetCore.Http; +using Microsoft.AspNetCore.Http; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace ForEvolve.ExceptionMapper; + +public class ExceptionHandlerTest +{ + public class KnowHowToHandleAsync : ExceptionHandlerTest + { + public static TheoryData TrueResults = new TheoryData + { + new TestException(), + new TestSubException() + }; + + public static TheoryData FalseResults = new TheoryData + { + new TestWrongException(), + new Exception() + }; + + [Theory] + [MemberData(nameof(TrueResults))] + public async Task Should_return_true_when_the_exception_is_a_TException(TestException exception) + { + var sut = new ExceptionTestHandler(); + var result = await sut.CanHandle(exception); + Assert.True(result); + } + + [Theory] + [MemberData(nameof(FalseResults))] + public async Task Should_return_false_when_the_exception_type_is_not_a_TException(Exception exception) + { + var sut = new ExceptionTestHandler(); + var result = await sut.CanHandle(exception); + Assert.False(result); + } + + } + + public class ExecuteAsync : ExceptionHandlerTest + { + private HttpContextHelper _httpContextHelper = new HttpContextHelper(); + + [Fact] + public async Task Should_set_response_StatusCode_to_handler_StatusCode_value() + { + var sut = new ExceptionTestHandler(); + var exception = new TestException(); + + await sut.ExecuteAsync(new ExceptionHandlingContext( + _httpContextHelper.HttpContextMock.Object, + exception, + default + )); + + Assert.Equal( + sut.StatusCode, + _httpContextHelper.HttpResponse.StatusCode + ); + } + + [Fact] + public async Task Should_call_ExecuteCoreAsync() + { + var sut = new ExceptionTestHandler(); + var exception = new TestException(); + + await sut.ExecuteAsync(new ExceptionHandlingContext( + _httpContextHelper.HttpContextMock.Object, + exception, + default + )); + + Assert.True(sut.HandleCoreWasCalled); + Assert.Same(exception, sut.Exception); + Assert.Same( + _httpContextHelper.HttpContextMock.Object, + sut.HttpContext + ); + } + } + + private class ExceptionTestHandler : ExceptionHandler + where TException : Exception + { + public override int StatusCode => 999; + + protected override Task ExecuteCoreAsync(ExceptionHandlingContext context) + { + HandleCoreWasCalled = true; + HttpContext = context.HttpContext; + Exception = context.Error; + return base.ExecuteCoreAsync(context); + } + + public bool HandleCoreWasCalled { get; private set; } + public TException Exception { get; private set; } + public HttpContext HttpContext { get; private set; } + } + + public class TestException : Exception { } + public class TestSubException : TestException { } + public class TestWrongException : Exception { } +} diff --git a/test/ForEvolve.ExceptionMapper.Tests/ExceptionHandlingManagerTest.cs b/test/ForEvolve.ExceptionMapper.Tests/ExceptionHandlingManagerTest.cs new file mode 100644 index 0000000..682467a --- /dev/null +++ b/test/ForEvolve.ExceptionMapper.Tests/ExceptionHandlingManagerTest.cs @@ -0,0 +1,134 @@ +using ForEvolve.Testing.AspNetCore.Http; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace ForEvolve.ExceptionMapper; + +public class ExceptionHandlingManagerTest +{ + public class HandleAsync : ExceptionHandlingManagerTest + { + private readonly HttpContextHelper _httpContextHelper = new HttpContextHelper(); + private HttpContext HttpContext => _httpContextHelper.HttpContextMock.Object; + private readonly List _handlers = new List(); + private readonly Mock _serializer = new Mock(); + private ExceptionMapperOptions Options => new(new ExceptionHandlerCollection(_handlers), _serializer.Object); + + public class When_IExceptionHandlerFeature_is_null : HandleAsync + { + [Fact] + public async Task Should_return_ExceptionHandlerFeatureNotSupportedResult() + { + // Arrange + var sut = new ExceptionHandlingManager(Options); + _httpContextHelper.FeaturesMock + .Setup(x => x.Get()) + .Returns(default(IExceptionHandlerFeature)); + + // Act + var result = await sut.HandleAsync(HttpContext); + + // Assert + Assert.IsType(result); + } + } + + public class When_IExceptionHandlerFeature_is_not_null : HandleAsync + { + private readonly Mock _exceptionHandlerFeatureMock; + + public When_IExceptionHandlerFeature_is_not_null() + { + _exceptionHandlerFeatureMock = new Mock(); + _httpContextHelper.FeaturesMock + .Setup(x => x.Get()) + .Returns(_exceptionHandlerFeatureMock.Object); + } + + public class And_has_no_Error : When_IExceptionHandlerFeature_is_not_null + { + [Fact] + public async Task Should_return_NoExceptionResult() + { + // Arrange + _exceptionHandlerFeatureMock + .Setup(x => x.Error) + .Returns(default(Exception)); + var sut = new ExceptionHandlingManager(Options); + + // Act + var result = await sut.HandleAsync(HttpContext); + + // Assert + Assert.IsType(result); + } + } + + public class And_has_an_Error : When_IExceptionHandlerFeature_is_not_null + { + private readonly Exception _exception; + public And_has_an_Error() + { + _exception = new Exception(); + _exceptionHandlerFeatureMock + .Setup(x => x.Error) + .Returns(_exception); + } + + public class And_the_Exception_was_handled : And_has_an_Error + { + [Fact] + public async Task Should_return_ExceptionHandlingContext_Result() + { + // Arrange + var handlerMock = new Mock(); + handlerMock + .Setup(x => x.CanHandle(_exception)) + .ReturnsAsync(true); + handlerMock + .Setup(x => x.ExecuteAsync(It.IsAny())) + .Callback((ExceptionHandlingContext context) => context.Result = new TestResult()) + .Returns(Task.CompletedTask); + _handlers.Add(handlerMock.Object); + var sut = new ExceptionHandlingManager(Options); + + // Act + var result = await sut.HandleAsync(HttpContext); + + // Assert + Assert.IsType(result); + } + + private class TestResult : IExceptionHandlingResult + { + public bool ExceptionHandled => throw new NotImplementedException(); + + public Exception Error => throw new NotImplementedException(); + } + } + + public class And_the_Exception_was_not_handled_by_any_handler : And_has_an_Error + { + [Fact] + public async Task Should_return_ExceptionNotHandledResult() + { + // Arrange + var sut = new ExceptionHandlingManager(Options); + + // Act + var result = await sut.HandleAsync(HttpContext); + + // Assert + Assert.IsType(result); + } + } + } + } + } +} diff --git a/test/ForEvolve.ExceptionMapper.Tests/ForEvolve.ExceptionMapper.Tests.csproj b/test/ForEvolve.ExceptionMapper.Tests/ForEvolve.ExceptionMapper.Tests.csproj index 8c189d6..cb9333e 100644 --- a/test/ForEvolve.ExceptionMapper.Tests/ForEvolve.ExceptionMapper.Tests.csproj +++ b/test/ForEvolve.ExceptionMapper.Tests/ForEvolve.ExceptionMapper.Tests.csproj @@ -1,11 +1,11 @@ - + ForEvolve.ExceptionMapper - net5.0;net6.0 - false + $(FETestsTargetFrameworks) + false diff --git a/test/ForEvolve.ExceptionMapper.Tests/HttpMiddleware/HttpExceptionHandlingMiddlewareTest.cs b/test/ForEvolve.ExceptionMapper.Tests/HttpMiddleware/HttpExceptionHandlingMiddlewareTest.cs new file mode 100644 index 0000000..9a822ff --- /dev/null +++ b/test/ForEvolve.ExceptionMapper.Tests/HttpMiddleware/HttpExceptionHandlingMiddlewareTest.cs @@ -0,0 +1,67 @@ +using ForEvolve.Testing.AspNetCore.Http; +using Microsoft.AspNetCore.Http; +using Moq; +using System.Threading.Tasks; +using Xunit; + +namespace ForEvolve.ExceptionMapper; + +public class HttpExceptionHandlingMiddlewareTest +{ + private readonly HttpContextHelper _httpContextHelper = new HttpContextHelper(); + private HttpContext HttpContext => _httpContextHelper.HttpContextMock.Object; + private bool _nextWasCalled = false; + private readonly HttpExceptionHandlingMiddleware sut; + private readonly Mock _exceptionHandlingManagerMock; + + public HttpExceptionHandlingMiddlewareTest() + { + _exceptionHandlingManagerMock = new Mock(); + sut = new HttpExceptionHandlingMiddleware( + _exceptionHandlingManagerMock.Object, + Next + ); + } + + [Fact] + public async Task Should_not_call_next_when_exception_is_handled() + { + // Arrange + var resultMock = new Mock(); + resultMock.Setup(x => x.ExceptionHandled).Returns(true); + _exceptionHandlingManagerMock + .Setup(x => x.HandleAsync(It.IsAny())) + .ReturnsAsync(resultMock.Object) + ; + + // Act + await sut.InvokeAsync(HttpContext); + + // Assert + Assert.False(_nextWasCalled); + } + + [Fact] + public async Task Should_call_next_when_exception_is_not_handled() + { + // Arrange + var resultMock = new Mock(); + resultMock.Setup(x => x.ExceptionHandled).Returns(false); + _exceptionHandlingManagerMock + .Setup(x => x.HandleAsync(It.IsAny())) + .ReturnsAsync(resultMock.Object) + ; + + // Act + await sut.InvokeAsync(HttpContext); + + // Assert + Assert.True(_nextWasCalled); + } + + private Task Next(HttpContext context) + { + _nextWasCalled = true; + return Task.CompletedTask; + } +} diff --git a/test/ForEvolve.ExceptionMapper.Tests/Results/ExceptionHandledResultTest.cs b/test/ForEvolve.ExceptionMapper.Tests/Results/ExceptionHandledResultTest.cs new file mode 100644 index 0000000..9b1f9fb --- /dev/null +++ b/test/ForEvolve.ExceptionMapper.Tests/Results/ExceptionHandledResultTest.cs @@ -0,0 +1,30 @@ +using System; +using Xunit; + +namespace ForEvolve.ExceptionMapper; + +public class ExceptionHandledResultTest +{ + [Fact] + public void Should_set_the_expected_values() + { + // Arrange + var exception = new Exception(); + + // Act + var result = new ExceptionHandledResult(exception); + + // Assert + Assert.Equal(exception, result.Error); + Assert.True(result.ExceptionHandled); + } + + [Fact] + public void Should_guard_against_null() + { + Assert.Throws( + "error", + () => new ExceptionHandledResult(default) + ); + } +} diff --git a/test/ForEvolve.ExceptionMapper.Tests/Results/ExceptionHandlerFeatureNotSupportedResultTest.cs b/test/ForEvolve.ExceptionMapper.Tests/Results/ExceptionHandlerFeatureNotSupportedResultTest.cs new file mode 100644 index 0000000..e65c5c2 --- /dev/null +++ b/test/ForEvolve.ExceptionMapper.Tests/Results/ExceptionHandlerFeatureNotSupportedResultTest.cs @@ -0,0 +1,17 @@ +using Xunit; + +namespace ForEvolve.ExceptionMapper; + +public class ExceptionHandlerFeatureNotSupportedResultTest +{ + [Fact] + public void Should_set_the_expected_values() + { + // Act + var result = new ExceptionHandlerFeatureNotSupportedResult(); + + // Assert + Assert.Null(result.Error); + Assert.False(result.ExceptionHandled); + } +} diff --git a/test/ForEvolve.ExceptionMapper.Tests/Results/ExceptionNotHandledResultTest.cs b/test/ForEvolve.ExceptionMapper.Tests/Results/ExceptionNotHandledResultTest.cs new file mode 100644 index 0000000..a3165dc --- /dev/null +++ b/test/ForEvolve.ExceptionMapper.Tests/Results/ExceptionNotHandledResultTest.cs @@ -0,0 +1,30 @@ +using System; +using Xunit; + +namespace ForEvolve.ExceptionMapper; + +public class ExceptionNotHandledResultTest +{ + [Fact] + public void Should_set_the_expected_values() + { + // Arrange + var exception = new Exception(); + + // Act + var result = new ExceptionNotHandledResult(exception); + + // Assert + Assert.Equal(exception, result.Error); + Assert.False(result.ExceptionHandled); + } + + [Fact] + public void Should_guard_against_null() + { + Assert.Throws( + "error", + () => new ExceptionNotHandledResult(default) + ); + } +} diff --git a/test/ForEvolve.ExceptionMapper.Tests/Results/NoExceptionResultTest.cs b/test/ForEvolve.ExceptionMapper.Tests/Results/NoExceptionResultTest.cs new file mode 100644 index 0000000..d8f5411 --- /dev/null +++ b/test/ForEvolve.ExceptionMapper.Tests/Results/NoExceptionResultTest.cs @@ -0,0 +1,17 @@ +using Xunit; + +namespace ForEvolve.ExceptionMapper; + +public class NoExceptionResultTest +{ + [Fact] + public void Should_set_the_expected_values() + { + // Act + var result = new NoExceptionResult(); + + // Assert + Assert.Null(result.Error); + Assert.False(result.ExceptionHandled); + } +} diff --git a/version.json b/version.json index a1a0c87..8d459ea 100644 --- a/version.json +++ b/version.json @@ -1,10 +1,12 @@ { "$schema": "https://raw.githubusercontent.com/AArnott/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "2.0", - "publicReleaseRefSpec": ["^refs/heads/master$"], + "version": "3.0", + "publicReleaseRefSpec": [ + "^refs/heads/master$" + ], "cloudBuild": { "buildNumber": { "enabled": true } } -} +} \ No newline at end of file