├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── dotnet-format.yml │ ├── dotnetcore.yml │ └── release.yaml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── Directory.Build.props ├── Directory.Packages.props ├── LICENSE ├── README.md ├── WeihanLi.Web.Extensions.sln.DotSettings ├── WeihanLi.Web.Extensions.slnx ├── azure-pipelines.yml ├── build.ps1 ├── build.sh ├── build ├── build.cs ├── version.props └── weihanli.snk ├── docfx.json ├── docs └── ReleaseNotes.md ├── icon.jpg ├── nuget.config ├── samples └── WeihanLi.Web.Extensions.Samples │ ├── AuthTestController.cs │ ├── Filters │ └── TestAuthFilter.cs │ ├── LogInterceptor.cs │ ├── McpToolExtension.cs │ ├── Program.cs │ ├── ValuesController.cs │ ├── WeihanLi.Web.Extensions.Samples.csproj │ ├── appsettings.Development.json │ └── appsettings.json ├── src ├── Directory.Build.props └── WeihanLi.Web.Extensions │ ├── AccessControlHelper │ ├── AccessControlAttribute.cs │ ├── AccessControlAuthorizationHandler.cs │ ├── AccessControlHelperBuilder.cs │ ├── AccessControlHelperConstants.cs │ ├── AccessControlHelperExtension.cs │ ├── AccessControlHelperMiddleware.cs │ ├── AccessControlOptions.cs │ ├── AccessControlRequirement.cs │ ├── AccessControlTagHelper.cs │ ├── HtmlHelperExtension.cs │ ├── IControlAccessStrategy.cs │ ├── IResourceAccessStrategy.cs │ ├── NoAccessControlAttribute.cs │ └── SparkContainer.cs │ ├── Authentication │ ├── ApiKeyAuthentication │ │ ├── ApiKeyAuthenticationDefaults.cs │ │ ├── ApiKeyAuthenticationHandler.cs │ │ └── ApiKeyAuthenticationOptions.cs │ ├── AuthenticationBuilderExtension.cs │ ├── BasicAuthentication │ │ ├── BasicAuthenticationDefaults.cs │ │ ├── BasicAuthenticationHandler.cs │ │ └── BasicAuthenticationOptions.cs │ ├── DelegateAuthentication │ │ ├── DelegateAuthenticationDefaults.cs │ │ ├── DelegateAuthenticationHandler.cs │ │ └── DelegateAuthenticationOptions.cs │ ├── HeaderAuthentication │ │ ├── HeaderAuthenticationDefaults.cs │ │ ├── HeaderAuthenticationHandler.cs │ │ └── HeaderAuthenticationOptions.cs │ └── QueryAuthentication │ │ ├── QueryAuthenticationDefaults.cs │ │ ├── QueryAuthenticationHandler.cs │ │ └── QueryAuthenticationOptions.cs │ ├── Authorization │ ├── Jwt │ │ ├── DependencyInjectionExtensions.cs │ │ ├── JsonWebTokenOptions.cs │ │ ├── JsonWebTokenOptionsSetup.cs │ │ ├── JsonWebTokenService.cs │ │ └── JwtBearerOptionsPostSetup.cs │ └── Token │ │ ├── ITokenService.cs │ │ └── TokenEntity.cs │ ├── DataProtection │ ├── DataProtectionBuilderExtensions.cs │ └── ParamsProtection │ │ ├── ParamsProtectionHelper.cs │ │ ├── ParamsProtectionOptions.cs │ │ ├── ParamsProtectionResourceFilter.cs │ │ └── ParamsProtectionResultFilter.cs │ ├── Extensions │ ├── DependenceResolverExtension.cs │ ├── EndpointExtensions.cs │ ├── FluentAspectServiceProviderFactory.cs │ ├── HealthCheckExtension.cs │ ├── HttpContextExtension.cs │ ├── HttpContextTenantProviderExtension.cs │ ├── HttpContextUserIdProviderExtension.cs │ ├── MiddlewareExtension.cs │ └── ResultExtensions.cs │ ├── Filters │ ├── ApiResultFilter.cs │ ├── AuthorizationFilterAttribute.cs │ ├── ConditionalFilter.cs │ ├── EnvironmentFilter.cs │ └── FeatureFlagFilter.cs │ ├── Formatters │ └── PlainTextInputFormatter.cs │ ├── Middleware │ ├── ConfigInspectorMiddleware.cs │ └── CustomExceptionHandlerMiddleware.cs │ ├── Pager │ ├── IPagedListModel.cs │ ├── IPagerModel.cs │ ├── PagedListModel.cs │ ├── PagedListModelExtension.cs │ ├── PagerHelper.cs │ └── PagerModel.cs │ ├── Services │ ├── HttpContextCancellationTokenProvider.cs │ ├── HttpContextLoggingEnricher.cs │ ├── HttpContextTenantProvider.cs │ └── HttpContextUserIdProvider.cs │ └── WeihanLi.Web.Extensions.csproj └── toc.yml /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome:http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Don't use tabs for indentation. 7 | [*] 8 | indent_style = space 9 | # (Please don't specify an indent_size here; that has too many unintended consequences.) 10 | 11 | # Code files 12 | [*.{cs,csx,vb,vbx}] 13 | indent_size = 4 14 | insert_final_newline = true 15 | charset = utf-8-bom 16 | 17 | # Xml project files 18 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] 19 | indent_size = 2 20 | 21 | # Xml config files 22 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] 23 | indent_size = 2 24 | 25 | # JSON files 26 | [*.json] 27 | indent_size = 2 28 | 29 | # Dotnet code style settings: 30 | [*.{cs,vb}] 31 | # File header 32 | file_header_template = Copyright (c) Weihan Li. All rights reserved.\nLicensed under the MIT license. 33 | 34 | # Sort using and Import directives with System.* appearing first 35 | dotnet_sort_system_directives_first = false 36 | # Avoid "this." and "Me." if not necessary 37 | dotnet_style_qualification_for_field = false:suggestion 38 | dotnet_style_qualification_for_property = false:suggestion 39 | dotnet_style_qualification_for_method = false:suggestion 40 | dotnet_style_qualification_for_event = false:suggestion 41 | 42 | # Use language keywords instead of framework type names for type references 43 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 44 | dotnet_style_predefined_type_for_member_access = true:suggestion 45 | 46 | # Suggest more modern language features when available 47 | dotnet_style_object_initializer = true:suggestion 48 | dotnet_style_collection_initializer = true:suggestion 49 | dotnet_style_coalesce_expression = true:suggestion 50 | dotnet_style_null_propagation = true:suggestion 51 | dotnet_style_explicit_tuple_names = true:suggestion 52 | 53 | # CSharp code style settings: 54 | [*.cs] 55 | # namespace style 56 | csharp_style_namespace_declarations=file_scoped:warning 57 | 58 | # Prefer "var" everywhere 59 | csharp_style_var_for_built_in_types = true:suggestion 60 | csharp_style_var_when_type_is_apparent = true:suggestion 61 | csharp_style_var_elsewhere = true:suggestion 62 | 63 | # Prefer method-like constructs to have a block body 64 | csharp_style_expression_bodied_methods = false:none 65 | csharp_style_expression_bodied_constructors = false:none 66 | csharp_style_expression_bodied_operators = false:none 67 | 68 | # Prefer property-like constructs to have an expression-body 69 | csharp_style_expression_bodied_properties = true:none 70 | csharp_style_expression_bodied_indexers = true:none 71 | csharp_style_expression_bodied_accessors = true:none 72 | 73 | # Suggest more modern language features when available 74 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 75 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 76 | csharp_style_inlined_variable_declaration = true:suggestion 77 | csharp_style_throw_expression = true:suggestion 78 | csharp_style_conditional_delegate_call = true:suggestion 79 | 80 | # Newline settings 81 | csharp_new_line_before_open_brace = all 82 | csharp_new_line_before_else = true 83 | csharp_new_line_before_catch = true 84 | csharp_new_line_before_finally = true 85 | csharp_new_line_before_members_in_object_initializers = true 86 | csharp_new_line_before_members_in_anonymous_types = true 87 | 88 | # Fix formatting, https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#rule-id-ide0055-fix-formatting 89 | dotnet_diagnostic.IDE00055.severity = warning 90 | # File header template 91 | dotnet_diagnostic.IDE0073.severity = warning 92 | 93 | # https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/unnecessary-code-rules 94 | # Remove unnecessary import 95 | dotnet_diagnostic.IDE0005.severity = warning 96 | # Private member is unused 97 | dotnet_diagnostic.IDE0051.severity = warning 98 | # Private member is unread 99 | dotnet_diagnostic.IDE0052.severity = warning 100 | 101 | # https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ 102 | # Avoid unused private fields 103 | dotnet_diagnostic.CA1823.severity = warning 104 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-format.yml: -------------------------------------------------------------------------------- 1 | name: dotnet-format 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | - "dev" 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Setup .NET SDK 15 | uses: actions/setup-dotnet@v4 16 | with: 17 | dotnet-version: | 18 | 8.0.x 19 | 9.0.x 20 | 10.0.x 21 | - name: format 22 | run: dotnet format 23 | - name: check for changes 24 | run: | 25 | if git diff --exit-code; then 26 | echo "has_changes=false" >> $GITHUB_ENV 27 | else 28 | echo "has_changes=true" >> $GITHUB_ENV 29 | fi 30 | - name: Commit and Push 31 | if: ${{ env.has_changes == 'true' }} 32 | shell: bash 33 | run: | 34 | # echo $GITHUB_REF_NAME 35 | # echo $GITHUB_SHA 36 | git config --local user.name "github-actions[bot]" 37 | git config --local user.email "weihanli@outlook.com" 38 | git add -u 39 | git commit -m "Automated dotnet-format update from commit ${GITHUB_SHA} on ${GITHUB_REF}" 40 | git log -1 41 | remote_repo="https://${GITHUB_ACTOR}:${{secrets.GITHUB_TOKEN}}@github.com/${GITHUB_REPOSITORY}.git" 42 | git push "${remote_repo}" HEAD:${GITHUB_REF} 43 | -------------------------------------------------------------------------------- /.github/workflows/dotnetcore.yml: -------------------------------------------------------------------------------- 1 | name: dotnetcore 2 | 3 | on: [push] 4 | 5 | jobs: 6 | # Label of the container job 7 | default: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Setup .NET SDK 12 | uses: actions/setup-dotnet@v4 13 | with: 14 | dotnet-version: | 15 | 8.0.x 16 | 9.0.x 17 | 10.0.x 18 | - name: dotnet info 19 | run: dotnet --info 20 | - name: build 21 | run: dotnet build 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: [ master ] 6 | jobs: 7 | build: 8 | name: Release 9 | runs-on: windows-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Setup .NET SDK 13 | uses: actions/setup-dotnet@v4 14 | with: 15 | dotnet-version: | 16 | 8.0.x 17 | 9.0.x 18 | 10.0.x 19 | - name: Build 20 | shell: pwsh 21 | run: .\build.ps1 --stable=true 22 | - name: Get Release Version 23 | shell: pwsh 24 | run: dotnet-exec https://github.com/OpenReservation/scripts/blob/main/build/export-gh-release-version.cs 25 | - name: create release 26 | shell: pwsh 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | run: | 30 | gh release create ${{ env.ReleaseVersion }} --generate-notes --target master (Get-Item ./artifacts/packages/*.nupkg) 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # .NET Core 46 | project.lock.json 47 | project.fragment.lock.json 48 | artifacts/ 49 | **/Properties/launchSettings.json 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # Visual Studio code coverage results 117 | *.coverage 118 | *.coveragexml 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | *.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignorable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | orleans.codegen.cs 203 | 204 | # Since there are multiple workflows, uncomment next line to ignore bower_components 205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 206 | #bower_components/ 207 | 208 | # RIA/Silverlight projects 209 | Generated_Code/ 210 | 211 | # Backup & report files from converting an old project file 212 | # to a newer Visual Studio version. Backup files are not needed, 213 | # because we have git ;-) 214 | _UpgradeReport_Files/ 215 | Backup*/ 216 | UpgradeLog*.XML 217 | UpgradeLog*.htm 218 | 219 | # SQL Server files 220 | *.mdf 221 | *.ldf 222 | *.ndf 223 | 224 | # Business Intelligence projects 225 | *.rdl.data 226 | *.bim.layout 227 | *.bim_*.settings 228 | 229 | # Microsoft Fakes 230 | FakesAssemblies/ 231 | 232 | # GhostDoc plugin setting file 233 | *.GhostDoc.xml 234 | 235 | # Node.js Tools for Visual Studio 236 | .ntvs_analysis.dat 237 | node_modules/ 238 | 239 | # Typescript v1 declaration files 240 | typings/ 241 | 242 | # Visual Studio 6 build log 243 | *.plg 244 | 245 | # Visual Studio 6 workspace options file 246 | *.opt 247 | 248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 249 | *.vbw 250 | 251 | # Visual Studio LightSwitch build output 252 | **/*.HTMLClient/GeneratedArtifacts 253 | **/*.DesktopClient/GeneratedArtifacts 254 | **/*.DesktopClient/ModelManifest.xml 255 | **/*.Server/GeneratedArtifacts 256 | **/*.Server/ModelManifest.xml 257 | _Pvt_Extensions 258 | 259 | # Paket dependency manager 260 | .paket/paket.exe 261 | paket-files/ 262 | 263 | # FAKE - F# Make 264 | .fake/ 265 | 266 | # JetBrains Rider 267 | .idea/ 268 | *.sln.iml 269 | 270 | # CodeRush 271 | .cr/ 272 | 273 | # Python Tools for Visual Studio (PTVS) 274 | __pycache__/ 275 | *.pyc 276 | 277 | # Cake - Uncomment if you are using it 278 | tools/** 279 | build/tools/** 280 | 281 | # Telerik's JustMock configuration file 282 | *.jmconfig 283 | 284 | # BizTalk build output 285 | *.btp.cs 286 | *.btm.cs 287 | *.odx.cs 288 | *.xsd.cs 289 | 290 | # docfx 291 | _site 292 | /**/DROP/ 293 | /**/TEMP/ 294 | docs/api/* -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "program": "${workspaceFolder}/samples/WeihanLi.Web.Extensions.Samples/bin/Debug/net7.0/WeihanLi.Web.Extensions.Samples.dll", 13 | "args": [], 14 | "cwd": "${workspaceFolder}/samples/WeihanLi.Web.Extensions.Samples", 15 | "stopAtEntry": false, 16 | "serverReadyAction": { 17 | "action": "openExternally", 18 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 19 | }, 20 | "env": { 21 | "ASPNETCORE_ENVIRONMENT": "Development" 22 | }, 23 | "sourceFileMap": { 24 | "/Views": "${workspaceFolder}/Views" 25 | } 26 | }, 27 | { 28 | "name": ".NET Core Attach", 29 | "type": "coreclr", 30 | "request": "attach" 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/samples/WeihanLi.Web.Extensions.Samples/WeihanLi.Web.Extensions.Samples.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/samples/WeihanLi.Web.Extensions.Samples/WeihanLi.Web.Extensions.Samples.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/samples/WeihanLi.Web.Extensions.Samples/WeihanLi.Web.Extensions.Samples.csproj" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | latest 5 | enable 6 | enable 7 | git 8 | https://github.com/WeihanLi/WeihanLi.Web.Extensions 9 | WeihanLi 10 | Copyright 2016-$([System.DateTime]::Now.Year) (c) WeihanLi 11 | $(NoWarn);NU5048;CS1591 12 | net10.0 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | true 5 | 6 | true 7 | all 8 | 9 | NU1901;NU1902;NU1903;NU1904 10 | 8.0.16 11 | 9.0.5 12 | 10.0.0-preview.4.25258.110 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017-2024 [WeihanLi] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WeihanLi.Web.Extensions 2 | 3 | ASP.NET Core Web extensions 4 | 5 | [![WeihanLi.Web.Extensions](https://img.shields.io/nuget/v/WeihanLi.Web.Extensions)](https://www.nuget.org/packages/WeihanLi.Web.Extensions/) 6 | 7 | [![WeihanLi.Web.Extensions](https://img.shields.io/nuget/vpre/WeihanLi.Web.Extensions)](https://www.nuget.org/packages/WeihanLi.Web.Extensions/absoluteLatest) 8 | 9 | [![Azure Pipelines Build Status](https://weihanli.visualstudio.com/Pipelines/_apis/build/status/WeihanLi.WeihanLi.Web.Extensions?branchName=dev)](https://weihanli.visualstudio.com/Pipelines/_build/latest?definitionId=19&branchName=dev) 10 | 11 | [![Github Actions Build Status](https://github.com/WeihanLi/WeihanLi.Web.Extensions/workflows/dotnetcore/badge.svg)](https://github.com/WeihanLi/WeihanLi.Web.Extensions/actions?query=workflow%3Adotnetcore) 12 | 13 | ## Features 14 | 15 | - MVC Pager 16 | - Data Protection 17 | - Authorization 18 | - Access Control Helper 19 | - JwtTokenService 20 | - Authentication 21 | - Query 22 | - Header 23 | - ApiKey 24 | - Basic 25 | - Delegate 26 | - Feature Flag 27 | - `HttpContext` extensions 28 | -------------------------------------------------------------------------------- /WeihanLi.Web.Extensions.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True -------------------------------------------------------------------------------- /WeihanLi.Web.Extensions.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - '*' # must quote since "*" is a YAML reserved character; we want a string 5 | 6 | pool: 7 | vmImage: 'windows-latest' 8 | 9 | steps: 10 | - task: UseDotNet@2 11 | displayName: 'Use .NET 8 sdk' 12 | inputs: 13 | packageType: sdk 14 | version: 8.0.x 15 | 16 | - task: UseDotNet@2 17 | displayName: 'Use .NET 9 sdk' 18 | inputs: 19 | packageType: sdk 20 | version: 9.0.x 21 | 22 | - task: UseDotNet@2 23 | displayName: 'Use .NET 10 sdk' 24 | inputs: 25 | packageType: sdk 26 | version: 10.0.x 27 | includePreviewVersions: true 28 | 29 | - script: dotnet --info 30 | displayName: 'dotnet info' 31 | 32 | - powershell: ./build.ps1 33 | displayName: 'Powershell Script' 34 | env: 35 | NuGet__ApiKey: $(nugetApiKey) 36 | NuGet__SourceUrl: $(nugetSourceUrl) 37 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | [string]$SCRIPT = '.\build\build.cs' 2 | 3 | # Install dotnet tool 4 | dotnet tool install --global dotnet-execute 5 | 6 | Write-Host "dotnet-exec $SCRIPT --args $ARGS" -ForegroundColor GREEN 7 | 8 | dotnet-exec $SCRIPT --args $ARGS 9 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | SCRIPT='./build/build.cs' 3 | 4 | # Install tool 5 | dotnet tool install --global dotnet-execute 6 | export PATH="$PATH:$HOME/.dotnet/tools" 7 | 8 | echo "dotnet-exec $SCRIPT --args=$@" 9 | 10 | dotnet-exec $SCRIPT --args="$@" 11 | -------------------------------------------------------------------------------- /build/build.cs: -------------------------------------------------------------------------------- 1 | var target = CommandLineParser.Val("target", args, "Default"); 2 | var apiKey = CommandLineParser.Val("apiKey", args); 3 | var stable = CommandLineParser.BooleanVal("stable", args); 4 | var noPush = CommandLineParser.BooleanVal("noPush", args); 5 | var branchName = EnvHelper.Val("BUILD_SOURCEBRANCHNAME", "local"); 6 | 7 | var solutionPath = "./WeihanLi.Web.Extensions.slnx"; 8 | string[] srcProjects = [ 9 | "./src/WeihanLi.Web.Extensions/WeihanLi.Web.Extensions.csproj" 10 | ]; 11 | string[] testProjects = [ "./test/WeihanLi.Web.Extensions.Test/WeihanLi.Web.Extensions.Test.csproj" ]; 12 | 13 | await new BuildProcessBuilder() 14 | .WithSetup(() => 15 | { 16 | // cleanup artifacts 17 | if (Directory.Exists("./artifacts/packages")) 18 | Directory.Delete("./artifacts/packages", true); 19 | 20 | // args 21 | Console.WriteLine("Arguments"); 22 | Console.WriteLine($" {args.StringJoin(" ")}"); 23 | }) 24 | .WithTaskExecuting(task => Console.WriteLine($@"===== Task {task.Name} {task.Description} executing ======")) 25 | .WithTaskExecuted(task => Console.WriteLine($@"===== Task {task.Name} {task.Description} executed ======")) 26 | .WithTask("hello", b => b.WithExecution(() => Console.WriteLine("Hello dotnet-exec build"))) 27 | .WithTask("build", b => 28 | { 29 | b.WithDescription("dotnet build") 30 | .WithExecution(() => ExecuteCommandAsync($"dotnet build {solutionPath}")) 31 | ; 32 | }) 33 | .WithTask("test", b => 34 | { 35 | b.WithDescription("dotnet test") 36 | .WithDependency("build") 37 | .WithExecution(async () => 38 | { 39 | foreach (var project in testProjects) 40 | { 41 | await ExecuteCommandAsync($"dotnet test --collect:\"XPlat Code Coverage;Format=cobertura,opencover;ExcludeByAttribute=ExcludeFromCodeCoverage,Obsolete,GeneratedCode,CompilerGeneratedAttribute\" {project}"); 42 | } 43 | }) 44 | ; 45 | }) 46 | .WithTask("pack", b => b.WithDescription("dotnet pack") 47 | .WithDependency("build") 48 | .WithExecution(async () => 49 | { 50 | if (stable || branchName == "master") 51 | { 52 | foreach (var project in srcProjects) 53 | { 54 | await ExecuteCommandAsync($"dotnet pack {project} -o ./artifacts/packages"); 55 | } 56 | } 57 | else 58 | { 59 | var suffix = $"preview-{DateTime.UtcNow:yyyyMMdd-HHmmss}"; 60 | foreach (var project in srcProjects) 61 | { 62 | await ExecuteCommandAsync($"dotnet pack {project} -o ./artifacts/packages --version-suffix {suffix}"); 63 | } 64 | } 65 | 66 | if (noPush) 67 | { 68 | Console.WriteLine("Skip push there's noPush specified"); 69 | return; 70 | } 71 | 72 | if (string.IsNullOrEmpty(apiKey)) 73 | { 74 | // try to get apiKey from environment variable 75 | apiKey = Environment.GetEnvironmentVariable("NuGet__ApiKey"); 76 | 77 | if (string.IsNullOrEmpty(apiKey)) 78 | { 79 | Console.WriteLine("Skip push since there's no apiKey found"); 80 | return; 81 | } 82 | } 83 | 84 | if (branchName != "master" && branchName != "preview") 85 | { 86 | Console.WriteLine($"Skip push since branch name {branchName} not support push packages"); 87 | return; 88 | } 89 | 90 | // push nuget packages 91 | var source = Environment.GetEnvironmentVariable("NuGet__SourceUrl"); 92 | var sourceConfig = string.IsNullOrEmpty(source) ? "" : $"-s {source}"; 93 | foreach (var file in Directory.GetFiles("./artifacts/packages/", "*.nupkg")) 94 | { 95 | await ExecuteCommandAsync($"dotnet nuget push {file} -k {apiKey} --skip-duplicate {sourceConfig}", [new("$NuGet__ApiKey", apiKey)]); 96 | } 97 | })) 98 | .WithTask("Default", b => b.WithDependency("hello").WithDependency("pack")) 99 | .Build() 100 | .ExecuteAsync(target, ApplicationHelper.ExitToken); 101 | 102 | async Task ExecuteCommandAsync(string commandText, KeyValuePair[]? replacements = null) 103 | { 104 | var commandTextWithReplacements = commandText; 105 | if (replacements is { Length: > 0}) 106 | { 107 | foreach (var item in replacements) 108 | { 109 | commandTextWithReplacements = commandTextWithReplacements.Replace(item.Value, item.Key); 110 | } 111 | } 112 | Console.WriteLine($"Executing command: \n {commandTextWithReplacements}"); 113 | Console.WriteLine(); 114 | var result = await CommandExecutor.ExecuteCommandAndOutputAsync(commandText); 115 | result.EnsureSuccessExitCode(); 116 | Console.WriteLine(); 117 | } 118 | -------------------------------------------------------------------------------- /build/version.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2 4 | 2 5 | 0 6 | $(VersionMajor).$(VersionMinor).$(VersionPatch) 7 | develop 8 | $(PackageVersion) 9 | 10 | 11 | -------------------------------------------------------------------------------- /build/weihanli.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeihanLi/WeihanLi.Web.Extensions/6b2a2bec9004785f68682275d552ee91ac4652bf/build/weihanli.snk -------------------------------------------------------------------------------- /docfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "src": [ 5 | { 6 | "src": "./src", 7 | "files": [ 8 | "**/*.csproj" 9 | ] 10 | } 11 | ], 12 | "dest": "docs/api" 13 | } 14 | ], 15 | "build": { 16 | "content": [ 17 | { 18 | "files": [ 19 | "**/*.{md,yml}" 20 | ], 21 | "exclude": [ 22 | "_site/**" 23 | ] 24 | } 25 | ], 26 | "resource": [ 27 | { 28 | "files": [ 29 | "images/**" 30 | ] 31 | } 32 | ], 33 | "output": "_site", 34 | "template": [ 35 | "default", 36 | "modern" 37 | ], 38 | "globalMetadata": { 39 | "_appName": "WeihanLi.Web.Extensions", 40 | "_appTitle": "WeihanLi.Web.Extensions", 41 | "_enableSearch": true, 42 | "pdf": false 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /docs/ReleaseNotes.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | see pull requests: https://github.com/WeihanLi/WeihanLi.Web.Extensions/pulls?q=is%3Apr+is%3Aclosed+is%3Amerged 4 | 5 | ## 1.4.0 6 | 7 | - Add Basic auth support 8 | - Add `net7.0` target 9 | - Support `IEndpointFilter` for `ApiResultFilter` 10 | - Add `ConditionalFilter`/`EnvironmentFilter`/`NonProductionEnvironmentFilter` 11 | 12 | ## 1.3.0 13 | 14 | - Add `ApiResultFilter` 15 | 16 | ## 1.2.1 17 | 18 | - add FluentAspectServiceProviderFactory 19 | - Add FeatureFlag extensions 20 | - Update HealthCheckExtension 21 | - Add Pager/DataProtection/AccessControlHelper 22 | - Add HttpContextTenantProvider 23 | - Add ApiKeyAuthentication Support, fixes Add API-Key based authentication #15 24 | - Add AuthorizationFilterAttribute 25 | - Implement JwtTokenService, fixes Simplify JWT usage #16 26 | 27 | ## 1.1.0 28 | 29 | - Header/Quey Auth 30 | - more web extensions 31 | 32 | ## [1.0.0](https://www.nuget.org/packages/WeihanLi.Web.Extensions/1.0.0) 33 | 34 | Init 35 | -------------------------------------------------------------------------------- /icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeihanLi/WeihanLi.Web.Extensions/6b2a2bec9004785f68682275d552ee91ac4652bf/icon.jpg -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /samples/WeihanLi.Web.Extensions.Samples/AuthTestController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Mvc; 5 | using WeihanLi.Web.Extensions.Samples.Filters; 6 | 7 | namespace WeihanLi.Web.Extensions.Samples; 8 | 9 | [TestAuthFilter(Role = "Admin")] 10 | [Route("api/authTest")] 11 | public class AuthTestController : ControllerBase 12 | { 13 | [TestAuthFilter(Role = "User")] 14 | [HttpGet] 15 | public IActionResult Index() 16 | { 17 | return Ok(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /samples/WeihanLi.Web.Extensions.Samples/Filters/TestAuthFilter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Mvc.Filters; 5 | using WeihanLi.Web.Filters; 6 | 7 | namespace WeihanLi.Web.Extensions.Samples.Filters; 8 | 9 | public class TestAuthFilter : AuthorizationFilterAttribute 10 | { 11 | public required string Role { get; set; } 12 | 13 | public override void OnAuthorization(AuthorizationFilterContext context) 14 | { 15 | if (!context.IsEffectivePolicy(this)) return; 16 | Console.WriteLine($"{nameof(TestAuthFilter)}({Role}) is executing"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /samples/WeihanLi.Web.Extensions.Samples/LogInterceptor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using System.Diagnostics; 5 | using WeihanLi.Common.Aspect; 6 | using WeihanLi.Extensions; 7 | 8 | namespace WeihanLi.Web.Extensions.Samples; 9 | 10 | public class EventPublishLogInterceptor : AbstractInterceptor 11 | { 12 | public override async Task Invoke(IInvocation invocation, Func next) 13 | { 14 | Console.WriteLine("-------------------------------"); 15 | Console.WriteLine($"Method {invocation.Method?.Name} invoke begin, eventData:{invocation.Arguments.ToJson()}"); 16 | var watch = Stopwatch.StartNew(); 17 | try 18 | { 19 | await next(); 20 | } 21 | catch (Exception ex) 22 | { 23 | Console.WriteLine($"Method {invocation.Method?.Name} invoke exception({ex})"); 24 | } 25 | finally 26 | { 27 | watch.Stop(); 28 | Console.WriteLine($"Method {invocation.Method?.Name} invoke complete, elasped:{watch.ElapsedMilliseconds} ms"); 29 | } 30 | Console.WriteLine("-------------------------------"); 31 | } 32 | } 33 | 34 | public class EventHandleLogInterceptor : IInterceptor 35 | { 36 | public async Task Invoke(IInvocation invocation, Func next) 37 | { 38 | Console.WriteLine("-------------------------------"); 39 | Console.WriteLine($"Event handle begin, eventData:{invocation.Arguments.ToJson()}"); 40 | var watch = Stopwatch.StartNew(); 41 | try 42 | { 43 | await next(); 44 | } 45 | catch (Exception ex) 46 | { 47 | Console.WriteLine($"Event handle exception({ex})"); 48 | } 49 | finally 50 | { 51 | watch.Stop(); 52 | Console.WriteLine($"Event handle complete, elasped:{watch.ElapsedMilliseconds} ms"); 53 | } 54 | Console.WriteLine("-------------------------------"); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /samples/WeihanLi.Web.Extensions.Samples/McpToolExtension.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.Extensions.DependencyInjection.Extensions; 5 | using ModelContextProtocol.Server; 6 | using System.Diagnostics; 7 | 8 | namespace WeihanLi.Web.Extensions.Samples; 9 | 10 | public interface IMcpToolEndpointMetadata 11 | { 12 | string Name { get; set; } 13 | string Description { get; set; } 14 | } 15 | 16 | public class McpToolEndpointMetadata : IMcpToolEndpointMetadata 17 | { 18 | public string Name { get; set; } = string.Empty; 19 | public string Description { get; set; } = string.Empty; 20 | } 21 | 22 | public class McpServerEndpointConfigureOptions(EndpointDataSource endpointDataSource, IServiceProvider services) : IConfigureOptions 23 | { 24 | public void Configure(McpServerOptions options) 25 | { 26 | options.Capabilities ??= new(); 27 | options.Capabilities.Tools.ToolCollection ??= new(); 28 | 29 | foreach (var endpoint in endpointDataSource.Endpoints) 30 | { 31 | if (!endpoint.Metadata.Any(m => m is IMcpToolEndpointMetadata)) 32 | continue; 33 | 34 | Debug.Assert(endpoint.RequestDelegate is not null); 35 | 36 | var tool = McpServerTool.Create(endpoint.RequestDelegate.Method, typeof(HttpContext), new McpServerToolCreateOptions 37 | { 38 | Services = services 39 | }); 40 | options.Capabilities.Tools.ToolCollection.Add(tool); 41 | } 42 | } 43 | } 44 | 45 | public static class McpToolExtension 46 | { 47 | public static IMcpServerBuilder WithEndpointTools(this IMcpServerBuilder builder) 48 | { 49 | ArgumentNullException.ThrowIfNull(builder); 50 | 51 | builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, McpServerEndpointConfigureOptions>()); 52 | return builder; 53 | } 54 | 55 | public static IEndpointConventionBuilder AsMcpTool(this TBuilder builder, Action? toolConfigure = null) 56 | where TBuilder : IEndpointConventionBuilder 57 | { 58 | ArgumentNullException.ThrowIfNull(builder); 59 | var metadata = new McpToolEndpointMetadata(); 60 | toolConfigure?.Invoke(metadata); 61 | builder.Add(c => 62 | { 63 | if (string.IsNullOrEmpty(metadata.Name)) 64 | { 65 | metadata.Name = c.DisplayName!; 66 | } 67 | if (string.IsNullOrEmpty(metadata.Description)) 68 | { 69 | metadata.Description = c.DisplayName!; 70 | } 71 | c.Metadata.Add(metadata); 72 | }); 73 | return builder; 74 | } 75 | } 76 | 77 | internal sealed class McpInvocation(IHttpContextAccessor contextAccessor) 78 | { 79 | 80 | } 81 | -------------------------------------------------------------------------------- /samples/WeihanLi.Web.Extensions.Samples/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.IdentityModel.Tokens; 5 | using Scalar.AspNetCore; 6 | using System.Security.Claims; 7 | using System.Text.Json.Serialization; 8 | using WeihanLi.Common.Aspect; 9 | using WeihanLi.Common.Models; 10 | using WeihanLi.Extensions; 11 | using WeihanLi.Web.Authentication; 12 | using WeihanLi.Web.Authentication.ApiKeyAuthentication; 13 | using WeihanLi.Web.Authentication.HeaderAuthentication; 14 | using WeihanLi.Web.Authorization.Jwt; 15 | using WeihanLi.Web.Extensions; 16 | using WeihanLi.Web.Extensions.Samples; 17 | using WeihanLi.Web.Filters; 18 | using WeihanLi.Web.Formatters; 19 | 20 | var builder = WebApplication.CreateBuilder(args); 21 | 22 | builder.Services.AddAuthentication(HeaderAuthenticationDefaults.AuthenticationScheme) 23 | .AddJwtBearer() 24 | .AddBasic(options => 25 | { 26 | options.UserName = "test"; 27 | options.Password = "test"; 28 | }) 29 | .AddQuery(options => { options.UserIdQueryKey = "uid"; }) 30 | .AddHeader(options => 31 | { 32 | options.UserIdHeaderName = "X-UserId"; 33 | options.UserNameHeaderName = "X-UserName"; 34 | options.UserRolesHeaderName = "X-UserRoles"; 35 | }) 36 | .AddApiKey(options => 37 | { 38 | options.ClaimsIssuer = "https://id.weihanli.xyz"; 39 | options.ApiKey = "123456"; 40 | options.ApiKeyName = "X-ApiKey"; 41 | options.KeyLocation = KeyLocation.HeaderOrQuery; 42 | }) 43 | .AddDelegate(options => 44 | { 45 | options.Validator = c => (c.Request.Headers.TryGetValue("x-delegate-key", out var values) && values.ToString().Equals("test")) 46 | .WrapTask(); 47 | options.ClaimsGenerator = c => 48 | { 49 | Claim[] claims = 50 | [ 51 | new (ClaimTypes.Name, "test") 52 | ]; 53 | return Task.FromResult>(claims); 54 | }; 55 | }) 56 | ; 57 | builder.Services.AddJwtServiceWithJwtBearerAuth(options => 58 | { 59 | options.SecretKey = Guid.NewGuid().ToString(); 60 | options.Issuer = "https://id.weihanli.xyz"; 61 | options.Audience = "SparkTodo"; 62 | // EnableRefreshToken, disabled by default 63 | options.EnableRefreshToken = true; 64 | // Renew refresh token always 65 | // options.RenewRefreshTokenPredicate = _ => true; 66 | options.RefreshTokenSigningCredentialsFactory = () => 67 | new SigningCredentials( 68 | new SymmetricSecurityKey(WeihanLi.Common.Services.GuidIdGenerator.Instance.NewId().GetBytes()), 69 | SecurityAlgorithms.HmacSha256 70 | ); 71 | }); 72 | builder.Services.AddAuthorization(options => 73 | { 74 | options.AddPolicy("Basic", 75 | policyBuilder => policyBuilder.AddAuthenticationSchemes("Basic").RequireAuthenticatedUser()); 76 | }); 77 | 78 | builder.Services.AddControllers(options => 79 | { 80 | options.InputFormatters.Add(new PlainTextInputFormatter()); 81 | }).AddJsonOptions(options => 82 | { 83 | options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; 84 | }); 85 | builder.Services.AddHttpContextUserIdProvider(options => 86 | { 87 | options.UserIdFactory = static context => $"{context.GetUserIP()}"; 88 | }); 89 | 90 | builder.Services.AddEndpointsApiExplorer(); 91 | builder.Services.AddOpenApi(); 92 | 93 | builder.Host.UseFluentAspectsServiceProviderFactory(options => 94 | { 95 | options.InterceptAll() 96 | .With(); 97 | }, ignoreTypesPredict: t => t.HasNamespace() && ( 98 | t.Namespace!.StartsWith("Microsoft.") 99 | || t.Namespace.StartsWith("System.") 100 | || t.Namespace.StartsWith("Swashbuckle.") 101 | ) 102 | ); 103 | 104 | builder.Services.AddMcpServer() 105 | .WithEndpointTools() 106 | .WithToolsFromAssembly() 107 | .WithHttpTransport() 108 | ; 109 | 110 | var app = builder.Build(); 111 | 112 | app.MapRuntimeInfo().ShortCircuit(); 113 | app.Map("/Hello", () => "Hello Minimal API!").AddEndpointFilter(); 114 | app.Map("/HelloV2", Hello).AddEndpointFilter(); 115 | app.Map("/HelloV3", () => Results.Ok(new { Name = "test" })).AddEndpointFilter(); 116 | app.Map("/HelloV4", () => Results.Ok(Result.Success(new { Name = "test" }))).AddEndpointFilter(); 117 | app.Map("/BadRequest", BadRequest).AddEndpointFilter(); 118 | app.Map("/basic-auth-test", () => "Hello").RequireAuthorization("Basic"); 119 | 120 | // conditional filter 121 | var conditionalTest = app.MapGroup("/conditional"); 122 | conditionalTest.Map("/NotFound", () => "Not Found") 123 | .AddEndpointFilter(new EnvironmentFilter("Production")); 124 | conditionalTest.Map("/Dynamic", () => "You get it") 125 | .AddEndpointFilter(new ConditionalFilter() 126 | { 127 | ConditionFunc = c => c.Request.Query.TryGetValue("enable", out _), 128 | ResultFactory = c => Results.NotFound(new { c.Request.QueryString }) 129 | }); 130 | 131 | var testGroup1 = app.MapGroup("/test1"); 132 | testGroup1.Map("/hello", () => "Hello"); 133 | testGroup1.Map("/world", () => "World"); 134 | testGroup1.AddEndpointFilter(); 135 | 136 | 137 | var envGroup = app.MapGroup("/env-test"); 138 | envGroup.Map("/dev", () => "env-test") 139 | .AddEndpointFilter(new EnvironmentFilter(Environments.Development)); 140 | envGroup.Map("/prod", () => "env-test") 141 | .AddEndpointFilter(new EnvironmentFilter(Environments.Production)); 142 | // attribute endpoint filter not supported for now, https://github.com/dotnet/aspnetcore/issues/43421 143 | // envGroup.Map("/stage", [EnvironmentFilter("Staging")]() => "env-test"); 144 | 145 | app.UseHealthCheck(); 146 | 147 | app.MapOpenApi(); 148 | app.MapScalarApiReference(); 149 | 150 | app.UseAuthentication(); 151 | app.UseAuthorization(); 152 | 153 | // app.MapConfigInspector(optionsConfigure: options => 154 | // { 155 | // options.ConfigRenderer = async (context, configs) => 156 | // { 157 | // var htmlStart = """ 158 | // 159 | // 160 | // Config Inspector 161 | // 162 | // 163 | // 164 | // 165 | // 166 | // 167 | // 168 | // 169 | // 170 | // 171 | // 172 | // 173 | // """; 174 | // var htmlEnd = "
ProviderKeyValueActive
"; 175 | // var tbody = new StringBuilder(); 176 | // foreach (var config in configs) 177 | // { 178 | // tbody.Append($"{config.Provider}"); 179 | // foreach (var item in config.Items) 180 | // { 181 | // tbody.Append( 182 | // $$"""{{item.Key}}{{item.Value}}"""); 183 | // } 184 | // 185 | // tbody.AppendLine(""); 186 | // } 187 | // 188 | // var responseText = $"{htmlStart}{tbody}{htmlEnd}"; 189 | // await context.Response.WriteAsync(responseText); 190 | // }; 191 | // }); 192 | 193 | app.MapConfigInspector() 194 | .AsMcpTool(tool => 195 | { 196 | tool.Description = "Get configurations"; 197 | }) 198 | // .RequireAuthorization(x => x 199 | // .AddAuthenticationSchemes("ApiKey") 200 | // .RequireAuthenticatedUser() 201 | // ) 202 | ; 203 | app.MapControllers(); 204 | 205 | app.MapGet("/endpoints", (EndpointDataSource endpointDataSource) => 206 | { 207 | var tools = endpointDataSource.Endpoints.Where(x => x.Metadata.Any(m => m is McpToolEndpointMetadata)) 208 | .Select(x => x.Metadata.OfType().First().Name) 209 | .ToArray(); 210 | return new 211 | { 212 | endpoints = endpointDataSource.Endpoints.Select(x => x.DisplayName), 213 | tools 214 | }; 215 | }); 216 | app.MapMcp(); 217 | 218 | await app.RunAsync(); 219 | 220 | 221 | static string Hello() => "Hello Minimal API!"; 222 | 223 | static IResult BadRequest() => Results.BadRequest(); 224 | -------------------------------------------------------------------------------- /samples/WeihanLi.Web.Extensions.Samples/ValuesController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Authentication.JwtBearer; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Mvc; 7 | using System.ComponentModel.DataAnnotations; 8 | using System.Security.Claims; 9 | using WeihanLi.Common.Models; 10 | using WeihanLi.Common.Services; 11 | using WeihanLi.Web.Authentication.ApiKeyAuthentication; 12 | using WeihanLi.Web.Authentication.DelegateAuthentication; 13 | using WeihanLi.Web.Authentication.HeaderAuthentication; 14 | using WeihanLi.Web.Authentication.QueryAuthentication; 15 | using WeihanLi.Web.Authorization.Token; 16 | using WeihanLi.Web.Filters; 17 | 18 | namespace WeihanLi.Web.Extensions.Samples; 19 | 20 | [Route("api/values")] 21 | [ApiController] 22 | [ApiResultFilter] 23 | public class ValuesController : ControllerBase 24 | { 25 | private readonly IServiceScopeFactory _serviceScopeFactory; 26 | 27 | public ValuesController(IServiceScopeFactory serviceScopeFactory) => _serviceScopeFactory = serviceScopeFactory; 28 | 29 | [HttpGet("[action]")] 30 | public IActionResult ServiceScopeTest() 31 | { 32 | Task.Run(() => 33 | { 34 | using var scope = _serviceScopeFactory.CreateScope(); 35 | var tokenService = scope.ServiceProvider.GetRequiredService(); 36 | Console.WriteLine(tokenService.GetHashCode()); 37 | }); 38 | return Ok(); 39 | } 40 | 41 | [HttpGet] 42 | public async Task Get([FromServices] IUserIdProvider userIdProvider) 43 | { 44 | var headerAuthResult = await HttpContext.AuthenticateAsync(HeaderAuthenticationDefaults.AuthenticationScheme); 45 | var queryAuthResult = await HttpContext.AuthenticateAsync(QueryAuthenticationDefaults.AuthenticationScheme); 46 | var apiKeyAuthResult = await HttpContext.AuthenticateAsync(ApiKeyAuthenticationDefaults.AuthenticationScheme); 47 | var delegateAuthResult = await HttpContext.AuthenticateAsync(DelegateAuthenticationDefaults.AuthenticationScheme); 48 | var bearerAuthResult = await HttpContext.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); 49 | 50 | return Ok(new 51 | { 52 | userId = userIdProvider.GetUserId(), 53 | defaultAuthResult = User.Identity, 54 | headerAuthResult = headerAuthResult.Principal?.Identity, 55 | queryAuthResult = queryAuthResult.Principal?.Identity, 56 | apiKeyAuthResult = apiKeyAuthResult.Principal?.Identity, 57 | bearerAuthResult = bearerAuthResult.Principal?.Identity, 58 | delegateAuthResult = delegateAuthResult.Principal?.Identity, 59 | }); 60 | } 61 | 62 | [HttpGet("apiKeyTest")] 63 | [Authorize(AuthenticationSchemes = ApiKeyAuthenticationDefaults.AuthenticationScheme)] 64 | public IActionResult ApiKeyAuthTest() 65 | { 66 | return Ok(User.Identity); 67 | } 68 | 69 | [HttpGet("[action]")] 70 | [FeatureFlagFilter("Flag1", DefaultValue = true)] 71 | public IActionResult FeatureEnableTest() 72 | { 73 | return Ok(new 74 | { 75 | Time = DateTime.UtcNow 76 | }); 77 | } 78 | 79 | [HttpGet("[action]")] 80 | [FeatureFlagFilter("Flag2", DefaultValue = false)] 81 | public IActionResult FeatureDisableTest() 82 | { 83 | return Ok(new 84 | { 85 | Time = DateTime.UtcNow 86 | }); 87 | } 88 | 89 | [HttpGet("getToken")] 90 | public async Task GetToken([Required] string userName, [FromServices] ITokenService tokenService) 91 | { 92 | var token = await tokenService 93 | .GenerateToken(new Claim("name", userName)); 94 | if (token is TokenEntityWithRefreshToken tokenEntityWithRefreshToken) 95 | { 96 | return tokenEntityWithRefreshToken.WrapResult().GetRestResult(); 97 | } 98 | return token.WrapResult().GetRestResult(); 99 | } 100 | 101 | [HttpGet("validateToken")] 102 | public async Task ValidateToken(string token, [FromServices] ITokenService tokenService) 103 | { 104 | return await tokenService 105 | .ValidateToken(token) 106 | .ContinueWith(r => 107 | r.Result.WrapResult().GetRestResult() 108 | ); 109 | } 110 | 111 | [HttpGet("RefreshToken")] 112 | public async Task RefreshToken(string refreshToken, [FromServices] ITokenService tokenService) 113 | { 114 | var token = await tokenService 115 | .RefreshToken(refreshToken); 116 | if (token is TokenEntityWithRefreshToken tokenEntityWithRefreshToken) 117 | { 118 | return tokenEntityWithRefreshToken.WrapResult().GetRestResult(); 119 | } 120 | return token.WrapResult().GetRestResult(); 121 | } 122 | 123 | [HttpGet("[action]")] 124 | [Authorize(AuthenticationSchemes = "Bearer")] 125 | public IActionResult BearerAuthTest() 126 | { 127 | return Ok(); 128 | } 129 | 130 | [HttpGet("[action]")] 131 | public IActionResult Test() 132 | { 133 | return Ok(new { Name = "Amazing .NET" }); 134 | } 135 | 136 | [HttpGet("[action]")] 137 | public IActionResult ExceptionTest() 138 | { 139 | throw new NotImplementedException(); 140 | } 141 | 142 | [HttpGet("EnvironmentFilterTest/Dev")] 143 | [EnvironmentFilter("Development")] 144 | //[EnvironmentFilter("Production")] 145 | public IActionResult EnvironmentFilterDevTest() 146 | { 147 | return Ok(new { Title = ".NET is amazing!" }); 148 | } 149 | 150 | [HttpGet("EnvironmentFilterTest/Prod")] 151 | [EnvironmentFilter("Production")] 152 | public IActionResult EnvironmentFilterProdTest() 153 | { 154 | return Ok(new { Title = ".NET is amazing!" }); 155 | } 156 | 157 | [HttpPost("RawTextFormatterTest")] 158 | [Consumes("text/plain")] 159 | public Result RawTextFormatterTest([FromBody] string input) 160 | { 161 | return Result.Success(input); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /samples/WeihanLi.Web.Extensions.Samples/WeihanLi.Web.Extensions.Samples.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /samples/WeihanLi.Web.Extensions.Samples/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "FeatureFlags": { 3 | "ConfigInspector": true 4 | } 5 | } -------------------------------------------------------------------------------- /samples/WeihanLi.Web.Extensions.Samples/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "FeatureFlags": { 3 | "ConfigInspector": false 4 | } 5 | } -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | direct 5 | false 6 | README.md 7 | icon.jpg 8 | 9 | true 10 | 11 | true 12 | snupkg 13 | 14 | https://github.com/WeihanLi/WeihanLi.Web.Extensions/blob/dev/docs/ReleaseNotes.md 15 | 16 | MIT 17 | false 18 | true 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/AccessControlHelper/AccessControlAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Mvc.Controllers; 5 | using Microsoft.AspNetCore.Mvc.Filters; 6 | using System.Reflection; 7 | using WeihanLi.Web.Filters; 8 | 9 | namespace WeihanLi.Web.AccessControlHelper; 10 | 11 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 12 | public sealed class AccessControlAttribute : AuthorizationFilterAttribute 13 | { 14 | public string? AccessKey { get; set; } 15 | 16 | public override void OnAuthorization(AuthorizationFilterContext filterContext) 17 | { 18 | ArgumentNullException.ThrowIfNull(filterContext); 19 | 20 | var isDefinedNoControl = filterContext.ActionDescriptor.IsDefined(typeof(NoAccessControlAttribute), true); 21 | 22 | if (isDefinedNoControl) return; 23 | 24 | var accessStrategy = filterContext.HttpContext.RequestServices.GetService(); 25 | if (accessStrategy is null) 26 | throw new InvalidOperationException("IResourceAccessStrategy not initialized,please register your ResourceAccessStrategy"); 27 | 28 | if (!accessStrategy.IsCanAccess(AccessKey)) 29 | { 30 | //if Ajax request 31 | filterContext.Result = filterContext.HttpContext.Request.IsAjaxRequest() ? 32 | accessStrategy.DisallowedAjaxResult : 33 | accessStrategy.DisallowedCommonResult; 34 | } 35 | } 36 | } 37 | 38 | internal static class AjaxRequestExtensions 39 | { 40 | public static bool IsAjaxRequest(this HttpRequest request) 41 | { 42 | return "XMLHttpRequest".Equals(request.Headers.XRequestedWith, StringComparison.OrdinalIgnoreCase); 43 | } 44 | 45 | public static bool IsDefined(this Microsoft.AspNetCore.Mvc.Abstractions.ActionDescriptor actionDescriptor, 46 | Type attributeType, bool inherit) 47 | { 48 | if (actionDescriptor is ControllerActionDescriptor controllerActionDescriptor) 49 | { 50 | if (controllerActionDescriptor.MethodInfo.GetCustomAttribute(attributeType) == null) 51 | { 52 | if (inherit && controllerActionDescriptor.ControllerTypeInfo.GetCustomAttribute(attributeType) != null) 53 | { 54 | return true; 55 | } 56 | } 57 | else 58 | { 59 | return true; 60 | } 61 | } 62 | return false; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/AccessControlHelper/AccessControlAuthorizationHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Authorization; 5 | using WeihanLi.Common; 6 | 7 | namespace WeihanLi.Web.AccessControlHelper; 8 | 9 | internal sealed class AccessControlAuthorizationHandler( 10 | IHttpContextAccessor contextAccessor, 11 | IOptions options) 12 | : AuthorizationHandler 13 | { 14 | private readonly AccessControlOptions _options = options.Value; 15 | 16 | protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AccessControlRequirement requirement) 17 | { 18 | var httpContext = contextAccessor.HttpContext; 19 | ArgumentNullException.ThrowIfNull(httpContext); 20 | var accessKey = _options.AccessKeyResolver.Invoke(httpContext); 21 | var resourceAccessStrategy = Guard.NotNull(httpContext).RequestServices.GetRequiredService(); 22 | if (resourceAccessStrategy.IsCanAccess(accessKey)) 23 | { 24 | context.Succeed(requirement); 25 | } 26 | else 27 | { 28 | context.Fail(); 29 | } 30 | return Task.CompletedTask; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/AccessControlHelper/AccessControlHelperBuilder.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | namespace WeihanLi.Web.AccessControlHelper; 5 | 6 | public interface IAccessControlHelperBuilder 7 | { 8 | IServiceCollection Services { get; } 9 | } 10 | 11 | internal sealed class AccessControlHelperBuilder(IServiceCollection services) : IAccessControlHelperBuilder 12 | { 13 | public IServiceCollection Services { get; } = services; 14 | } 15 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/AccessControlHelper/AccessControlHelperConstants.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | namespace WeihanLi.Web.AccessControlHelper; 5 | 6 | public static class AccessControlHelperConstants 7 | { 8 | public const string PolicyName = "AccessControl"; 9 | } 10 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/AccessControlHelper/AccessControlHelperExtension.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.Extensions.DependencyInjection.Extensions; 6 | using WeihanLi.Web.AccessControlHelper; 7 | 8 | namespace Microsoft.AspNetCore.Builder 9 | { 10 | public static class ApplicationBuilderExtensions 11 | { 12 | public static IApplicationBuilder UseAccessControlHelper(this IApplicationBuilder app) 13 | { 14 | ArgumentNullException.ThrowIfNull(app); 15 | return app.UseMiddleware(); 16 | } 17 | } 18 | } 19 | 20 | namespace Microsoft.Extensions.DependencyInjection 21 | { 22 | public static class ServiceCollectionExtensions 23 | { 24 | public static IAccessControlHelperBuilder AddAccessControlHelper(this IServiceCollection services) 25 | where TResourceAccessStrategy : class, IResourceAccessStrategy 26 | where TControlStrategy : class, IControlAccessStrategy 27 | { 28 | ArgumentNullException.ThrowIfNull(services); 29 | 30 | services.TryAddSingleton(); 31 | services.TryAddSingleton(); 32 | 33 | return services.AddAccessControlHelper(); 34 | } 35 | 36 | public static IAccessControlHelperBuilder AddAccessControlHelper(this IServiceCollection services, ServiceLifetime resourceAccessStrategyLifetime, ServiceLifetime controlAccessStrategyLifetime) 37 | where TResourceAccessStrategy : class, IResourceAccessStrategy 38 | where TControlStrategy : class, IControlAccessStrategy 39 | { 40 | ArgumentNullException.ThrowIfNull(services); 41 | 42 | services.TryAdd(new ServiceDescriptor(typeof(IResourceAccessStrategy), typeof(TResourceAccessStrategy), resourceAccessStrategyLifetime)); 43 | services.TryAdd(new ServiceDescriptor(typeof(IControlAccessStrategy), typeof(TControlStrategy), controlAccessStrategyLifetime)); 44 | 45 | return services.AddAccessControlHelper(); 46 | } 47 | 48 | public static IAccessControlHelperBuilder AddAccessControlHelper( 49 | this IServiceCollection services, Action configAction) 50 | where TResourceAccessStrategy : class, IResourceAccessStrategy 51 | where TControlStrategy : class, IControlAccessStrategy 52 | { 53 | ArgumentNullException.ThrowIfNull(services); 54 | ArgumentNullException.ThrowIfNull(configAction); 55 | 56 | services.Configure(configAction); 57 | return services.AddAccessControlHelper(); 58 | } 59 | 60 | public static IAccessControlHelperBuilder AddAccessControlHelper(this IServiceCollection services, Action configAction, ServiceLifetime resourceAccessStrategyLifetime, ServiceLifetime controlAccessStrategyLifetime) 61 | where TResourceAccessStrategy : class, IResourceAccessStrategy 62 | where TControlStrategy : class, IControlAccessStrategy 63 | { 64 | ArgumentNullException.ThrowIfNull(services); 65 | ArgumentNullException.ThrowIfNull(configAction); 66 | 67 | services.Configure(configAction); 68 | return services.AddAccessControlHelper(resourceAccessStrategyLifetime, controlAccessStrategyLifetime); 69 | } 70 | 71 | public static IAccessControlHelperBuilder AddAccessControlHelper(this IServiceCollection services, bool useAsDefaultPolicy) 72 | { 73 | ArgumentNullException.ThrowIfNull(services); 74 | 75 | if (useAsDefaultPolicy) 76 | { 77 | services.AddAuthorization(options => 78 | { 79 | var accessControlPolicy = new AuthorizationPolicyBuilder() 80 | .AddRequirements(new AccessControlRequirement()) 81 | .Build(); 82 | options.AddPolicy(AccessControlHelperConstants.PolicyName, accessControlPolicy); 83 | options.DefaultPolicy = accessControlPolicy; 84 | }); 85 | } 86 | else 87 | { 88 | services.AddAuthorization(options => 89 | { 90 | var accessControlPolicy = new AuthorizationPolicyBuilder() 91 | .AddRequirements(new AccessControlRequirement()) 92 | .Build(); 93 | options.AddPolicy(AccessControlHelperConstants.PolicyName, accessControlPolicy); 94 | }); 95 | } 96 | 97 | services.AddSingleton(); 98 | 99 | services.TryAddSingleton(); 100 | 101 | return new AccessControlHelperBuilder(services); 102 | } 103 | 104 | public static IAccessControlHelperBuilder AddAccessControlHelper(this IServiceCollection services) 105 | { 106 | return AddAccessControlHelper(services, false); 107 | } 108 | 109 | public static IAccessControlHelperBuilder AddAccessControlHelper(this IServiceCollection services, Action configAction) 110 | { 111 | ArgumentNullException.ThrowIfNull(services); 112 | ArgumentNullException.ThrowIfNull(configAction); 113 | 114 | var option = new AccessControlOptions(); 115 | configAction.Invoke(option); 116 | services.Configure(configAction); 117 | return services.AddAccessControlHelper(option.UseAsDefaultPolicy); 118 | } 119 | 120 | public static IAccessControlHelperBuilder AddResourceAccessStrategy(this IAccessControlHelperBuilder builder) where TResourceAccessStrategy : IResourceAccessStrategy 121 | { 122 | return AddResourceAccessStrategy(builder, ServiceLifetime.Singleton); 123 | } 124 | 125 | public static IAccessControlHelperBuilder AddResourceAccessStrategy(this IAccessControlHelperBuilder builder, ServiceLifetime serviceLifetime) where TResourceAccessStrategy : IResourceAccessStrategy 126 | { 127 | if (null == builder) 128 | { 129 | throw new ArgumentNullException(nameof(builder)); 130 | } 131 | 132 | builder.Services.Add( 133 | new ServiceDescriptor(typeof(IResourceAccessStrategy), typeof(TResourceAccessStrategy), serviceLifetime)); 134 | return builder; 135 | } 136 | 137 | public static IAccessControlHelperBuilder AddControlAccessStrategy(this IAccessControlHelperBuilder builder) where TControlAccessStrategy : IControlAccessStrategy 138 | { 139 | return AddControlAccessStrategy(builder, ServiceLifetime.Singleton); 140 | } 141 | 142 | public static IAccessControlHelperBuilder AddControlAccessStrategy(this IAccessControlHelperBuilder builder, ServiceLifetime serviceLifetime) where TControlAccessStrategy : IControlAccessStrategy 143 | { 144 | if (null == builder) 145 | { 146 | throw new ArgumentNullException(nameof(builder)); 147 | } 148 | 149 | builder.Services.Add(new ServiceDescriptor(typeof(IControlAccessStrategy), typeof(TControlAccessStrategy), serviceLifetime)); 150 | return builder; 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/AccessControlHelper/AccessControlHelperMiddleware.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | namespace WeihanLi.Web.AccessControlHelper; 5 | 6 | /// 7 | /// AccessControlHelperMiddleware 8 | /// 9 | internal sealed class AccessControlHelperMiddleware 10 | { 11 | private readonly RequestDelegate _next; 12 | private readonly AccessControlOptions _option; 13 | 14 | /// 15 | /// Creates a new instance of 16 | /// 17 | /// The delegate representing the next middleware in the request pipeline. 18 | /// 19 | public AccessControlHelperMiddleware( 20 | RequestDelegate next, 21 | IOptions options 22 | ) 23 | { 24 | ArgumentNullException.ThrowIfNull(next); 25 | _next = next; 26 | _option = options.Value; 27 | } 28 | 29 | /// 30 | /// Executes the middleware. 31 | /// 32 | /// The for the current request. 33 | /// A task that represents the execution of this middleware. 34 | public Task Invoke(HttpContext context) 35 | { 36 | var accessKey = _option.AccessKeyResolver.Invoke(context); 37 | var accessStrategy = context.RequestServices.GetRequiredService(); 38 | if (accessStrategy.IsCanAccess(accessKey)) 39 | { 40 | return _next(context); 41 | } 42 | 43 | context.Response.StatusCode = context.User is { Identity.IsAuthenticated: true } ? 403 : 401; 44 | return _option.DefaultUnauthorizedOperation?.Invoke(context) ?? Task.CompletedTask; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/AccessControlHelper/AccessControlOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | namespace WeihanLi.Web.AccessControlHelper; 5 | 6 | public sealed class AccessControlOptions 7 | { 8 | private Func _accessKeyResolver = context => 9 | context.Request.Headers.TryGetValue("X-Access-Key", out var val) ? val.ToString() : null; 10 | public bool UseAsDefaultPolicy { get; set; } 11 | 12 | public Func AccessKeyResolver 13 | { 14 | get => _accessKeyResolver; 15 | set => _accessKeyResolver = value ?? throw new ArgumentNullException(nameof(AccessKeyResolver)); 16 | } 17 | 18 | public Func? DefaultUnauthorizedOperation { get; set; } 19 | } 20 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/AccessControlHelper/AccessControlRequirement.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Authorization; 5 | 6 | namespace WeihanLi.Web.AccessControlHelper; 7 | 8 | internal sealed class AccessControlRequirement : IAuthorizationRequirement 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/AccessControlHelper/AccessControlTagHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Razor.TagHelpers; 5 | 6 | namespace WeihanLi.Web.AccessControlHelper; 7 | 8 | /// 9 | /// AccessControlTagHelper 10 | /// add support for tagHelper 11 | /// https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/authoring?view=aspnetcore-2.1#condition-tag-helper 12 | /// 13 | [HtmlTargetElement(Attributes = "asp-access")] 14 | public sealed class AccessControlTagHelper(IControlAccessStrategy controlAccessStrategy) : TagHelper 15 | { 16 | public override void Process(TagHelperContext context, TagHelperOutput output) 17 | { 18 | context.AllAttributes.TryGetAttribute("asp-access-key", out var accessKey); 19 | if (!controlAccessStrategy.IsControlCanAccess(accessKey?.Value.ToString())) 20 | { 21 | output.SuppressOutput(); 22 | } 23 | else 24 | { 25 | if (accessKey != null) 26 | { 27 | output.Attributes.Remove(accessKey); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/AccessControlHelper/HtmlHelperExtension.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Html; 5 | using Microsoft.AspNetCore.Mvc.Rendering; 6 | using Microsoft.AspNetCore.Mvc.ViewFeatures; 7 | 8 | namespace WeihanLi.Web.AccessControlHelper; 9 | 10 | public static class HtmlHelperExtension 11 | { 12 | /// 13 | /// SparkActionLink 14 | /// 15 | /// HtmlHelper 16 | /// linkText 17 | /// actionName 18 | /// controllerName 19 | /// routeValues 20 | /// htmlAttributes 21 | /// accessKey 22 | /// 23 | public static IHtmlContent SparkActionLink( 24 | this IHtmlHelper helper, string linkText, string actionName, string? controllerName = null, 25 | object? routeValues = null, object? htmlAttributes = null, string? accessKey = null) 26 | { 27 | if (helper.ViewContext.HttpContext.RequestServices.GetRequiredService() 28 | .IsControlCanAccess(accessKey)) 29 | { 30 | if (string.IsNullOrEmpty(controllerName)) 31 | { 32 | return helper.ActionLink(linkText, actionName, routeValues, htmlAttributes); 33 | } 34 | else 35 | { 36 | return helper.ActionLink(linkText, actionName, controllerName, routeValues, htmlAttributes); 37 | } 38 | } 39 | return HtmlString.Empty; 40 | } 41 | 42 | /// 43 | /// SparkContainer 44 | /// 45 | /// HtmlHelper 46 | /// tagName 47 | /// htmlAttributes 48 | /// accessKey 49 | /// 50 | public static SparkContainer SparkContainer(this IHtmlHelper helper, string tagName, object? attributes = null, string? accessKey = null) 51 | => SparkContainerHelper(helper, tagName, HtmlHelper.AnonymousObjectToHtmlAttributes(attributes), accessKey); 52 | 53 | private static SparkContainer SparkContainerHelper(IHtmlHelper helper, string tagName, 54 | IDictionary? attributes = null, string? accessKey = null) 55 | { 56 | var tagBuilder = new TagBuilder(tagName); 57 | var canAccess = helper.ViewContext.HttpContext.RequestServices.GetRequiredService() 58 | .IsControlCanAccess(accessKey); 59 | if (canAccess) 60 | { 61 | if (attributes is not null) 62 | { 63 | tagBuilder.MergeAttributes(attributes); 64 | } 65 | 66 | tagBuilder.TagRenderMode = TagRenderMode.StartTag; 67 | helper.ViewContext.Writer.Write(tagBuilder); 68 | } 69 | return new SparkContainer(helper.ViewContext, tagName, canAccess); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/AccessControlHelper/IControlAccessStrategy.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | namespace WeihanLi.Web.AccessControlHelper; 5 | 6 | /// 7 | /// view component access strategy 8 | /// 9 | public interface IControlAccessStrategy 10 | { 11 | /// 12 | /// view component access strategy 13 | /// 14 | bool IsControlCanAccess(string? accessKey); 15 | } 16 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/AccessControlHelper/IResourceAccessStrategy.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace WeihanLi.Web.AccessControlHelper; 7 | 8 | public interface IResourceAccessStrategy 9 | { 10 | /// 11 | /// Is resource can be accessed 12 | /// 13 | /// accessKey 14 | /// 15 | bool IsCanAccess(string? accessKey); 16 | 17 | /// 18 | /// AccessStrategyName 19 | /// 20 | //string StrategyName { get; } 21 | 22 | IActionResult DisallowedCommonResult { get; } 23 | 24 | IActionResult DisallowedAjaxResult { get; } 25 | } 26 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/AccessControlHelper/NoAccessControlAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Mvc.Filters; 5 | 6 | namespace WeihanLi.Web.AccessControlHelper; 7 | 8 | /// 9 | /// NoAccessControl 10 | /// 11 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 12 | public sealed class NoAccessControlAttribute : Attribute, IAuthorizationFilter 13 | { 14 | public void OnAuthorization(AuthorizationFilterContext context) 15 | { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/AccessControlHelper/SparkContainer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Mvc.Rendering; 5 | 6 | namespace WeihanLi.Web.AccessControlHelper; 7 | 8 | public sealed class SparkContainer : IDisposable 9 | { 10 | private readonly string _tagName; 11 | private readonly ViewContext _viewContext; 12 | private bool _disposed; 13 | private TextWriter? _writer; 14 | 15 | public SparkContainer(ViewContext viewContext, string tagName, bool canAccess = true) 16 | { 17 | _viewContext = viewContext; 18 | _tagName = tagName; 19 | if (!canAccess) 20 | { 21 | _writer = viewContext.Writer; 22 | viewContext.Writer = TextWriter.Null; 23 | } 24 | } 25 | 26 | public void Dispose() 27 | { 28 | if (!_disposed) 29 | { 30 | _disposed = true; 31 | EndContainer(); 32 | } 33 | } 34 | 35 | public void EndContainer() 36 | { 37 | if (_writer is not null) 38 | { 39 | _viewContext.Writer = _writer; 40 | _writer = null; 41 | } 42 | else 43 | { 44 | _viewContext.Writer.Write("", _tagName); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Authentication/ApiKeyAuthentication/ApiKeyAuthenticationDefaults.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | namespace WeihanLi.Web.Authentication.ApiKeyAuthentication; 5 | 6 | public static class ApiKeyAuthenticationDefaults 7 | { 8 | public const string AuthenticationScheme = "ApiKey"; 9 | } 10 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Authentication/ApiKeyAuthentication/ApiKeyAuthenticationHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.Extensions.Primitives; 6 | using System.Security.Claims; 7 | using System.Text.Encodings.Web; 8 | 9 | namespace WeihanLi.Web.Authentication.ApiKeyAuthentication; 10 | 11 | public sealed class ApiKeyAuthenticationHandler : AuthenticationHandler 12 | { 13 | public ApiKeyAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) 14 | : base(options, logger, encoder) 15 | { 16 | } 17 | 18 | protected override async Task HandleAuthenticateAsync() 19 | { 20 | StringValues keyValues; 21 | var keyExists = Options.KeyLocation switch 22 | { 23 | KeyLocation.Query => Request.Query.TryGetValue(Options.ApiKeyName, out keyValues), 24 | KeyLocation.HeaderOrQuery => Request.Headers.TryGetValue(Options.ApiKeyName, out keyValues) || Request.Query.TryGetValue(Options.ApiKeyName, out keyValues), 25 | KeyLocation.QueryOrHeader => Request.Query.TryGetValue(Options.ApiKeyName, out keyValues) || Request.Headers.TryGetValue(Options.ApiKeyName, out keyValues), 26 | _ => Request.Headers.TryGetValue(Options.ApiKeyName, out keyValues), 27 | }; 28 | if (!keyExists) 29 | return AuthenticateResult.NoResult(); 30 | 31 | var validator = Options.ApiKeyValidator ?? ((_, keyValue) => Task.FromResult(string.Equals(Options.ApiKey, keyValue))); 32 | if (!await validator.Invoke(Context, keyValues.ToString())) 33 | return AuthenticateResult.Fail("Invalid ApiKey"); 34 | 35 | var claims = new[] 36 | { 37 | new Claim("issuer", ClaimsIssuer) 38 | }; 39 | 40 | if (Options.ClaimsGenerator != null) 41 | { 42 | var generatedClaims = await Options.ClaimsGenerator.Invoke(Context, Options); 43 | if (generatedClaims is { Count: > 0 }) 44 | { 45 | claims = claims.Union(generatedClaims).ToArray(); 46 | } 47 | } 48 | 49 | return AuthenticateResult.Success( 50 | new AuthenticationTicket( 51 | new ClaimsPrincipal([ 52 | new ClaimsIdentity(claims, Scheme.Name) 53 | ]), Scheme.Name) 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Authentication/ApiKeyAuthentication/ApiKeyAuthenticationOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using System.Security.Claims; 5 | 6 | namespace WeihanLi.Web.Authentication.ApiKeyAuthentication; 7 | 8 | public sealed class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions 9 | { 10 | public string ApiKey { get; set; } = default!; 11 | public string ApiKeyName { get; set; } = "X-ApiKey"; 12 | public KeyLocation KeyLocation { get; set; } 13 | public Func>? ApiKeyValidator { get; set; } 14 | public Func>>? ClaimsGenerator { get; set; } 15 | 16 | public override void Validate() 17 | { 18 | ArgumentException.ThrowIfNullOrWhiteSpace(ApiKey); 19 | } 20 | } 21 | 22 | public enum KeyLocation 23 | { 24 | Header = 0, 25 | Query = 1, 26 | HeaderOrQuery = 2, 27 | QueryOrHeader = 3, 28 | } 29 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Authentication/AuthenticationBuilderExtension.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using WeihanLi.Web.Authentication.ApiKeyAuthentication; 5 | using WeihanLi.Web.Authentication.BasicAuthentication; 6 | using WeihanLi.Web.Authentication.DelegateAuthentication; 7 | using WeihanLi.Web.Authentication.HeaderAuthentication; 8 | using WeihanLi.Web.Authentication.QueryAuthentication; 9 | 10 | namespace WeihanLi.Web.Authentication; 11 | 12 | public static class AuthenticationBuilderExtension 13 | { 14 | #region AddApiKey 15 | 16 | public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder) 17 | { 18 | return builder.AddApiKey(ApiKeyAuthenticationDefaults.AuthenticationScheme); 19 | } 20 | 21 | public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder, string schema) 22 | { 23 | return builder.AddApiKey(schema, _ => { }); 24 | } 25 | 26 | public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder, 27 | Action configureOptions) 28 | { 29 | return builder.AddApiKey(ApiKeyAuthenticationDefaults.AuthenticationScheme, 30 | configureOptions); 31 | } 32 | 33 | public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder, string schema, 34 | Action configureOptions) 35 | { 36 | ArgumentNullException.ThrowIfNull(builder); 37 | ArgumentNullException.ThrowIfNull(configureOptions); 38 | 39 | builder.Services.Configure(configureOptions); 40 | return builder.AddScheme(schema, 41 | configureOptions); 42 | } 43 | 44 | #endregion AddApiKey 45 | 46 | #region AddBasic 47 | 48 | public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder) 49 | { 50 | return builder.AddBasic(BasicAuthenticationDefaults.AuthenticationScheme); 51 | } 52 | 53 | public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, string schema) 54 | { 55 | return builder.AddBasic(schema, _ => { }); 56 | } 57 | 58 | public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, 59 | Action configureOptions) 60 | { 61 | return builder.AddBasic(BasicAuthenticationDefaults.AuthenticationScheme, 62 | configureOptions); 63 | } 64 | 65 | public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, string schema, 66 | Action configureOptions) 67 | { 68 | ArgumentNullException.ThrowIfNull(builder); 69 | ArgumentNullException.ThrowIfNull(configureOptions); 70 | 71 | builder.Services.Configure(configureOptions); 72 | return builder.AddScheme(schema, 73 | configureOptions); 74 | } 75 | 76 | #endregion AddApiKey 77 | 78 | #region AddHeader 79 | 80 | public static AuthenticationBuilder AddHeader(this AuthenticationBuilder builder) 81 | { 82 | return builder.AddHeader(HeaderAuthenticationDefaults.AuthenticationScheme); 83 | } 84 | 85 | public static AuthenticationBuilder AddHeader(this AuthenticationBuilder builder, string schema) 86 | { 87 | return builder.AddHeader(schema, _ => { }); 88 | } 89 | 90 | public static AuthenticationBuilder AddHeader(this AuthenticationBuilder builder, 91 | Action configureOptions) 92 | { 93 | return builder.AddHeader(HeaderAuthenticationDefaults.AuthenticationScheme, 94 | configureOptions); 95 | } 96 | 97 | public static AuthenticationBuilder AddHeader(this AuthenticationBuilder builder, string schema, 98 | Action configureOptions) 99 | { 100 | ArgumentNullException.ThrowIfNull(builder); 101 | ArgumentNullException.ThrowIfNull(configureOptions); 102 | 103 | builder.Services.Configure(configureOptions); 104 | return builder.AddScheme(schema, 105 | configureOptions); 106 | } 107 | 108 | #endregion AddHeader 109 | 110 | #region AddQuery 111 | 112 | public static AuthenticationBuilder AddQuery(this AuthenticationBuilder builder) 113 | { 114 | return builder.AddQuery(QueryAuthenticationDefaults.AuthenticationScheme); 115 | } 116 | 117 | public static AuthenticationBuilder AddQuery(this AuthenticationBuilder builder, string schema) 118 | { 119 | return builder.AddQuery(schema, _ => { }); 120 | } 121 | 122 | public static AuthenticationBuilder AddQuery(this AuthenticationBuilder builder, 123 | Action configureOptions) 124 | { 125 | return builder.AddQuery(QueryAuthenticationDefaults.AuthenticationScheme, 126 | configureOptions); 127 | } 128 | 129 | public static AuthenticationBuilder AddQuery(this AuthenticationBuilder builder, string schema, 130 | Action configureOptions) 131 | { 132 | ArgumentNullException.ThrowIfNull(builder); 133 | ArgumentNullException.ThrowIfNull(configureOptions); 134 | 135 | builder.Services.Configure(configureOptions); 136 | return builder.AddScheme(schema, 137 | configureOptions); 138 | } 139 | 140 | #endregion AddQuery 141 | 142 | #region AddDelegate 143 | 144 | public static AuthenticationBuilder AddDelegate(this AuthenticationBuilder builder, 145 | Action configureOptions) => 146 | AddDelegate(builder, DelegateAuthenticationDefaults.AuthenticationScheme, configureOptions); 147 | 148 | public static AuthenticationBuilder AddDelegate(this AuthenticationBuilder builder, string schema, 149 | Action configureOptions) 150 | { 151 | ArgumentNullException.ThrowIfNull(builder); 152 | ArgumentNullException.ThrowIfNull(configureOptions); 153 | 154 | builder.Services.Configure(configureOptions); 155 | return builder.AddScheme(schema, 156 | configureOptions); 157 | } 158 | 159 | #endregion AddDelegate 160 | } 161 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Authentication/BasicAuthentication/BasicAuthenticationDefaults.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | namespace WeihanLi.Web.Authentication.BasicAuthentication; 5 | 6 | public static class BasicAuthenticationDefaults 7 | { 8 | public const string AuthenticationScheme = "Basic"; 9 | } 10 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Authentication/BasicAuthentication/BasicAuthenticationHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.Extensions.Logging; 5 | using System.Security.Claims; 6 | using System.Text.Encodings.Web; 7 | using WeihanLi.Extensions; 8 | using AuthenticateResult = Microsoft.AspNetCore.Authentication.AuthenticateResult; 9 | 10 | namespace WeihanLi.Web.Authentication.BasicAuthentication; 11 | 12 | public sealed class BasicAuthenticationHandler : AuthenticationHandler 13 | { 14 | public BasicAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) 15 | : base(options, logger, encoder) 16 | { 17 | } 18 | 19 | protected override async Task HandleAuthenticateAsync() 20 | { 21 | if (!Request.Headers.TryGetValue("Authorization", out var authHeaderValues)) 22 | { 23 | return AuthenticateResult.NoResult(); 24 | } 25 | 26 | var authHeader = authHeaderValues.ToString(); 27 | if (authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase)) 28 | { 29 | var token = authHeader[("Basic ".Length)..]; 30 | if (string.IsNullOrEmpty(token)) 31 | { 32 | return AuthenticateResult.Fail("Invalid Authorization header"); 33 | } 34 | 35 | string userName, password; 36 | try 37 | { 38 | var array = Convert.FromBase64String(token).GetString().Split(':'); 39 | if (array.Length != 2) 40 | { 41 | return AuthenticateResult.Fail("Invalid Authorization header"); 42 | } 43 | 44 | userName = array[0]; 45 | password = array[1]; 46 | } 47 | catch 48 | { 49 | return AuthenticateResult.Fail("Invalid Authorization header"); 50 | } 51 | 52 | 53 | var valid = await Options.UserCredentialValidator.Invoke(Request.HttpContext, userName, password); 54 | if (valid) 55 | { 56 | var claims = new[] 57 | { 58 | new Claim(ClaimTypes.Name, userName), 59 | new Claim("issuer", ClaimsIssuer), 60 | }; 61 | return AuthenticateResult.Success(new AuthenticationTicket( 62 | new ClaimsPrincipal([ 63 | new ClaimsIdentity(claims, Scheme.Name) 64 | ]), Scheme.Name)); 65 | } 66 | return AuthenticateResult.Fail("Invalid user credential"); 67 | } 68 | return AuthenticateResult.NoResult(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Authentication/BasicAuthentication/BasicAuthenticationOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | namespace WeihanLi.Web.Authentication.BasicAuthentication; 5 | 6 | public sealed class BasicAuthenticationOptions : AuthenticationSchemeOptions 7 | { 8 | public string? UserName { get; set; } 9 | public string? Password { get; set; } 10 | 11 | public Func> UserCredentialValidator { get; set; } 12 | = (context, user, pass) => 13 | { 14 | var options = context.RequestServices.GetRequiredService>() 15 | .Value; 16 | return Task.FromResult(user == options.UserName && pass == options.Password); 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Authentication/DelegateAuthentication/DelegateAuthenticationDefaults.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | namespace WeihanLi.Web.Authentication.DelegateAuthentication; 5 | 6 | public static class DelegateAuthenticationDefaults 7 | { 8 | public const string AuthenticationScheme = "Delegate"; 9 | } 10 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Authentication/DelegateAuthentication/DelegateAuthenticationHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.Extensions.Logging; 5 | using System.Security.Claims; 6 | using System.Text.Encodings.Web; 7 | 8 | namespace WeihanLi.Web.Authentication.DelegateAuthentication; 9 | 10 | public sealed class DelegateAuthenticationHandler( 11 | IOptionsMonitor options, 12 | ILoggerFactory logger, 13 | UrlEncoder encoder) 14 | : AuthenticationHandler(options, logger, encoder) 15 | { 16 | protected override async Task HandleAuthenticateAsync() 17 | { 18 | ArgumentNullException.ThrowIfNull(Options.Validator); 19 | 20 | var authenticated = await Options.Validator.Invoke(Context); 21 | if (!authenticated) 22 | return AuthenticateResult.Fail($"Delegate authentication({Scheme.DisplayName ?? Scheme.Name}) failed."); 23 | 24 | List claims = 25 | [ 26 | new("issuer", ClaimsIssuer) 27 | ]; 28 | 29 | if (Options.ClaimsGenerator != null) 30 | { 31 | var generatedClaims = await Options.ClaimsGenerator.Invoke(Context); 32 | if (generatedClaims is { Count: > 0 }) 33 | { 34 | claims = [.. generatedClaims, .. claims]; 35 | } 36 | } 37 | 38 | return AuthenticateResult.Success( 39 | new AuthenticationTicket( 40 | new ClaimsPrincipal([ 41 | new ClaimsIdentity(claims, Scheme.Name) 42 | ]), Scheme.Name) 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Authentication/DelegateAuthentication/DelegateAuthenticationOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using System.Security.Claims; 5 | 6 | namespace WeihanLi.Web.Authentication.DelegateAuthentication; 7 | 8 | public sealed class DelegateAuthenticationOptions : AuthenticationSchemeOptions 9 | { 10 | public Func> Validator { get; set; } = _ => Task.FromResult(false); 11 | public Func>>? ClaimsGenerator { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Authentication/HeaderAuthentication/HeaderAuthenticationDefaults.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | namespace WeihanLi.Web.Authentication.HeaderAuthentication; 5 | 6 | public static class HeaderAuthenticationDefaults 7 | { 8 | public const string AuthenticationScheme = "Header"; 9 | } 10 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Authentication/HeaderAuthentication/HeaderAuthenticationHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.Extensions.Logging; 5 | using System.Security.Claims; 6 | using System.Text.Encodings.Web; 7 | 8 | namespace WeihanLi.Web.Authentication.HeaderAuthentication; 9 | 10 | public sealed class HeaderAuthenticationHandler : AuthenticationHandler 11 | { 12 | public HeaderAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) 13 | : base(options, logger, encoder) 14 | { 15 | } 16 | 17 | protected override async Task HandleAuthenticateAsync() 18 | { 19 | if (await Options.AuthenticationValidator(Context)) 20 | { 21 | var claims = new List(); 22 | if (Request.Headers.TryGetValue(Options.UserIdHeaderName, out var userIdValues)) 23 | { 24 | claims.Add(new Claim(ClaimTypes.NameIdentifier, userIdValues.ToString())); 25 | } 26 | if (Request.Headers.TryGetValue(Options.UserNameHeaderName, out var userNameValues)) 27 | { 28 | claims.Add(new Claim(ClaimTypes.Name, userNameValues.ToString())); 29 | } 30 | if (Request.Headers.TryGetValue(Options.UserRolesHeaderName, out var userRolesValues)) 31 | { 32 | var userRoles = userRolesValues.ToString() 33 | .Split([Options.Delimiter], StringSplitOptions.RemoveEmptyEntries); 34 | claims.AddRange(userRoles.Select(r => new Claim(ClaimTypes.Role, r))); 35 | } 36 | 37 | if (Options.AdditionalHeaderToClaims.Count > 0) 38 | { 39 | foreach (var headerToClaim in Options.AdditionalHeaderToClaims) 40 | { 41 | if (Request.Headers.TryGetValue(headerToClaim.Key, out var headerValues)) 42 | { 43 | foreach (var val in headerValues.ToString().Split([Options.Delimiter], StringSplitOptions.RemoveEmptyEntries)) 44 | { 45 | claims.Add(new Claim(headerToClaim.Value, val)); 46 | } 47 | } 48 | } 49 | } 50 | 51 | // claims identity 's authentication type can not be null https://stackoverflow.com/questions/45261732/user-identity-isauthenticated-always-false-in-net-core-custom-authentication 52 | var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, Scheme.Name)); 53 | var ticket = new AuthenticationTicket( 54 | principal, 55 | Scheme.Name 56 | ); 57 | return AuthenticateResult.Success(ticket); 58 | } 59 | 60 | return AuthenticateResult.NoResult(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Authentication/HeaderAuthentication/HeaderAuthenticationOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | namespace WeihanLi.Web.Authentication.HeaderAuthentication; 5 | 6 | public sealed class HeaderAuthenticationOptions : AuthenticationSchemeOptions 7 | { 8 | private string _userRolesHeaderName = "UserRoles"; 9 | private string _userNameHeaderName = "UserName"; 10 | private string _userIdHeaderName = "UserId"; 11 | private string _delimiter = ","; 12 | 13 | public string UserIdHeaderName 14 | { 15 | get => _userIdHeaderName; 16 | set 17 | { 18 | if (!string.IsNullOrWhiteSpace(value)) 19 | { 20 | _userIdHeaderName = value; 21 | } 22 | } 23 | } 24 | 25 | public string UserNameHeaderName 26 | { 27 | get => _userNameHeaderName; 28 | set 29 | { 30 | if (!string.IsNullOrWhiteSpace(value)) 31 | { 32 | _userNameHeaderName = value; 33 | } 34 | } 35 | } 36 | 37 | public string UserRolesHeaderName 38 | { 39 | get => _userRolesHeaderName; 40 | set 41 | { 42 | if (!string.IsNullOrWhiteSpace(value)) 43 | { 44 | _userRolesHeaderName = value; 45 | } 46 | } 47 | } 48 | 49 | /// 50 | /// AdditionalHeaderToClaims 51 | /// key: headerName 52 | /// value: claimType 53 | /// 54 | public Dictionary AdditionalHeaderToClaims { get; } = new(StringComparer.OrdinalIgnoreCase); 55 | 56 | private Func> _authenticationValidator = context => 57 | { 58 | var userIdHeader = context.RequestServices.GetRequiredService>().Value.UserIdHeaderName; 59 | return Task.FromResult(context.Request.Headers.ContainsKey(userIdHeader)); 60 | }; 61 | 62 | public Func> AuthenticationValidator 63 | { 64 | get => _authenticationValidator; 65 | set => _authenticationValidator = value ?? throw new ArgumentNullException(nameof(AuthenticationValidator)); 66 | } 67 | 68 | public string Delimiter 69 | { 70 | get => _delimiter; 71 | set => _delimiter = string.IsNullOrEmpty(value) ? "," : value; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Authentication/QueryAuthentication/QueryAuthenticationDefaults.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | namespace WeihanLi.Web.Authentication.QueryAuthentication; 5 | 6 | public static class QueryAuthenticationDefaults 7 | { 8 | public const string AuthenticationScheme = "Query"; 9 | } 10 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Authentication/QueryAuthentication/QueryAuthenticationHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.Extensions.Logging; 5 | using System.Security.Claims; 6 | using System.Text.Encodings.Web; 7 | 8 | namespace WeihanLi.Web.Authentication.QueryAuthentication; 9 | 10 | public sealed class QueryAuthenticationHandler : AuthenticationHandler 11 | { 12 | public QueryAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) 13 | : base(options, logger, encoder) 14 | { 15 | } 16 | 17 | protected override async Task HandleAuthenticateAsync() 18 | { 19 | if (Request.Query.Count > 0 && await Options.AuthenticationValidator(Context)) 20 | { 21 | var claims = new List(); 22 | if (Request.Query.TryGetValue(Options.UserIdQueryKey, out var userIdValues)) 23 | { 24 | claims.Add(new Claim(ClaimTypes.NameIdentifier, userIdValues.ToString())); 25 | } 26 | if (Request.Query.TryGetValue(Options.UserNameQueryKey, out var userNameValues)) 27 | { 28 | claims.Add(new Claim(ClaimTypes.Name, userNameValues.ToString())); 29 | } 30 | if (Request.Query.TryGetValue(Options.UserRolesQueryKey, out var userRolesValues)) 31 | { 32 | var userRoles = userRolesValues.ToString() 33 | .Split([Options.Delimiter], StringSplitOptions.RemoveEmptyEntries); 34 | claims.AddRange(userRoles.Select(r => new Claim(ClaimTypes.Role, r))); 35 | } 36 | if (Options.AdditionalQueryToClaims.Count > 0) 37 | { 38 | foreach (var queryToClaim in Options.AdditionalQueryToClaims) 39 | { 40 | if (Request.Query.TryGetValue(queryToClaim.Key, out var queryValues)) 41 | { 42 | foreach (var val in queryValues.ToString().Split([Options.Delimiter], StringSplitOptions.RemoveEmptyEntries)) 43 | { 44 | claims.Add(new Claim(queryToClaim.Value, val)); 45 | } 46 | } 47 | } 48 | } 49 | // claims identity 's authentication type can not be null https://stackoverflow.com/questions/45261732/user-identity-isauthenticated-always-false-in-net-core-custom-authentication 50 | var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, Scheme.Name)); 51 | var ticket = new AuthenticationTicket(principal, Scheme.Name); 52 | return AuthenticateResult.Success(ticket); 53 | } 54 | 55 | return AuthenticateResult.NoResult(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Authentication/QueryAuthentication/QueryAuthenticationOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | namespace WeihanLi.Web.Authentication.QueryAuthentication; 5 | 6 | public sealed class QueryAuthenticationOptions : AuthenticationSchemeOptions 7 | { 8 | private string _userRolesQueryKey = "UserRoles"; 9 | private string _userNameQueryKey = "UserName"; 10 | private string _userIdQueryKey = "UserId"; 11 | private string _delimiter = ","; 12 | 13 | public string UserIdQueryKey 14 | { 15 | get => _userIdQueryKey; 16 | set 17 | { 18 | if (!string.IsNullOrWhiteSpace(value)) 19 | { 20 | _userIdQueryKey = value; 21 | } 22 | } 23 | } 24 | 25 | public string UserNameQueryKey 26 | { 27 | get => _userNameQueryKey; 28 | set 29 | { 30 | if (!string.IsNullOrWhiteSpace(value)) 31 | { 32 | _userNameQueryKey = value; 33 | } 34 | } 35 | } 36 | 37 | public string UserRolesQueryKey 38 | { 39 | get => _userRolesQueryKey; 40 | set 41 | { 42 | if (!string.IsNullOrWhiteSpace(value)) 43 | { 44 | _userRolesQueryKey = value; 45 | } 46 | } 47 | } 48 | 49 | /// 50 | /// 自定义其他的 header 51 | /// key: QueryKey 52 | /// value: claimType 53 | /// 54 | public Dictionary AdditionalQueryToClaims { get; } = new(StringComparer.OrdinalIgnoreCase); 55 | 56 | public string Delimiter 57 | { 58 | get => _delimiter; 59 | set 60 | { 61 | if (string.IsNullOrEmpty(value)) 62 | { 63 | _delimiter = value; 64 | } 65 | } 66 | } 67 | 68 | private Func> _authenticationValidator = context => 69 | { 70 | var userIdKey = context.RequestServices.GetRequiredService>().Value.UserIdQueryKey; 71 | return Task.FromResult(context.Request.Query.ContainsKey(userIdKey)); 72 | }; 73 | 74 | public Func> AuthenticationValidator 75 | { 76 | get => _authenticationValidator; 77 | set => _authenticationValidator = value ?? throw new ArgumentNullException(nameof(AuthenticationValidator)); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Authorization/Jwt/DependencyInjectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Authentication.JwtBearer; 5 | using Microsoft.Extensions.DependencyInjection.Extensions; 6 | using WeihanLi.Web.Authorization.Token; 7 | 8 | namespace WeihanLi.Web.Authorization.Jwt; 9 | 10 | public static class DependencyInjectionExtensions 11 | { 12 | public static IServiceCollection AddJwtService(this IServiceCollection serviceCollection, Action optionsAction) 13 | { 14 | ArgumentNullException.ThrowIfNull(serviceCollection); 15 | ArgumentNullException.ThrowIfNull(optionsAction); 16 | 17 | serviceCollection.Configure(optionsAction); 18 | serviceCollection.TryAddSingleton(); 19 | serviceCollection.ConfigureOptions(); 20 | return serviceCollection; 21 | } 22 | 23 | public static IServiceCollection AddJwtServiceWithJwtBearerAuth(this IServiceCollection serviceCollection, Action optionsAction, Action? jwtBearerOptionsSetup = null) 24 | { 25 | ArgumentNullException.ThrowIfNull(serviceCollection); 26 | ArgumentNullException.ThrowIfNull(optionsAction); 27 | 28 | if (jwtBearerOptionsSetup is not null) 29 | { 30 | serviceCollection.Configure(jwtBearerOptionsSetup); 31 | } 32 | 33 | serviceCollection.ConfigureOptions(); 34 | return serviceCollection.AddJwtService(optionsAction); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Authorization/Jwt/JsonWebTokenOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.IdentityModel.Tokens; 5 | using WeihanLi.Common.Services; 6 | 7 | namespace WeihanLi.Web.Authorization.Jwt; 8 | 9 | public sealed class JsonWebTokenOptions 10 | { 11 | /// 12 | /// "iss" (Issuer) Claim 13 | /// 14 | /// The "iss" (issuer) claim identifies the principal that issued the 15 | /// JWT. The processing of this claim is generally application specific. 16 | /// The "iss" value is a case-sensitive string containing a StringOrURI 17 | /// value. Use of this claim is OPTIONAL. 18 | public string? Issuer { get; set; } 19 | 20 | /// 21 | /// "aud" (Audience) Claim 22 | /// 23 | /// The "aud" (audience) claim identifies the recipients that the JWT is 24 | /// intended for. Each principal intended to process the JWT MUST 25 | /// identify itself with a value in the audience claim. If the principal 26 | /// processing the claim does not identify itself with a value in the 27 | /// "aud" claim when this claim is present, then the JWT MUST be 28 | /// rejected. In the general case, the "aud" value is an array of case-sensitive strings, each containing a StringOrURI value. 29 | /// In the special case when the JWT has one audience, the "aud" value MAY be a 30 | /// single case-sensitive string containing a StringOrURI value. The 31 | /// interpretation of audience values is generally application specific. 32 | /// Use of this claim is OPTIONAL. 33 | public string? Audience { get; set; } 34 | 35 | /// 36 | /// SecretKey used for generate and validate token 37 | /// 38 | public string? SecretKey { get; set; } 39 | 40 | /// 41 | /// Set the timespan the token will be valid for (default is 1 hour/3600 seconds) 42 | /// 43 | public TimeSpan ValidFor { get; set; } = TimeSpan.FromHours(1); 44 | 45 | /// 46 | /// Gets or sets the clock skew to apply when validating a time. 47 | /// 48 | public TimeSpan ClockSkew { get; set; } = TimeSpan.FromMinutes(5); 49 | 50 | /// 51 | /// "jti" (JWT ID) Claim (default ID is a GUID) 52 | /// 53 | /// 54 | /// The "jti" (JWT ID) claim provides a unique identifier for the JWT. 55 | /// The identifier value MUST be assigned in a manner that ensures that 56 | /// there is a negligible probability that the same value will be 57 | /// accidentally assigned to a different data object; if the application 58 | /// uses multiple issuers, collisions MUST be prevented among values 59 | /// produced by different issuers as well. The "jti" claim can be used 60 | /// to prevent the JWT from being replayed. 61 | /// 62 | public Func? JtiGenerator { get; set; } = () => GuidIdGenerator.Instance.NewId(); 63 | 64 | public Func? SigningCredentialsFactory { get; set; } 65 | 66 | public bool EnableRefreshToken { get; set; } 67 | 68 | public Func? RenewRefreshTokenPredicate { get; set; } 69 | 70 | public TimeSpan RefreshTokenValidFor { get; set; } = TimeSpan.FromHours(8); 71 | 72 | public Func? RefreshTokenSigningCredentialsFactory { get; set; } 73 | 74 | public string? NameClaimType { get; set; } 75 | public string? RoleClaimType { get; set; } 76 | 77 | public string RefreshTokenOwnerClaimType { get; set; } = "x-rt-owner"; 78 | 79 | public Func? RefreshTokenValidator { get; set; } 80 | 81 | public Action? TokenValidationConfigure { get; set; } 82 | 83 | internal SigningCredentials? SigningCredentials { get; set; } 84 | internal SigningCredentials? RefreshTokenSigningCredentials { get; set; } 85 | 86 | internal TokenValidationParameters GetTokenValidationParameters(Action? parametersAction = null) 87 | { 88 | var parameters = new TokenValidationParameters 89 | { 90 | // The signing key must match! 91 | ValidateIssuerSigningKey = true, 92 | IssuerSigningKey = SigningCredentials?.Key, 93 | // Validate the JWT Issuer (iss) claim 94 | ValidateIssuer = true, 95 | ValidIssuer = Issuer, 96 | // Validate the JWT Audience (aud) claim 97 | ValidateAudience = true, 98 | ValidAudience = Audience, 99 | // Validate the token expiry 100 | ValidateLifetime = true, 101 | // If you want to allow a certain amount of clock drift, set that here: 102 | ClockSkew = ClockSkew 103 | }; 104 | 105 | if (!string.IsNullOrEmpty(NameClaimType)) 106 | { 107 | parameters.NameClaimType = NameClaimType; 108 | } 109 | if (!string.IsNullOrEmpty(RoleClaimType)) 110 | { 111 | parameters.RoleClaimType = RoleClaimType; 112 | } 113 | 114 | parametersAction?.Invoke(parameters); 115 | TokenValidationConfigure?.Invoke(parameters); 116 | return parameters; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Authorization/Jwt/JsonWebTokenOptionsSetup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.IdentityModel.Tokens; 5 | using WeihanLi.Extensions; 6 | 7 | namespace WeihanLi.Web.Authorization.Jwt; 8 | 9 | internal sealed class JsonWebTokenOptionsSetup : IPostConfigureOptions 10 | { 11 | public void PostConfigure(string? name, JsonWebTokenOptions options) 12 | { 13 | if (options.SigningCredentialsFactory is null) 14 | { 15 | if (options.SecretKey.IsNotNullOrWhiteSpace()) 16 | { 17 | options.SigningCredentialsFactory = () => new SigningCredentials(new SymmetricSecurityKey(options.SecretKey.GetBytes()), SecurityAlgorithms.HmacSha256); 18 | } 19 | } 20 | ArgumentNullException.ThrowIfNull(options.SigningCredentialsFactory); 21 | options.SigningCredentials = options.SigningCredentialsFactory.Invoke(); 22 | options.RefreshTokenSigningCredentials = options.RefreshTokenSigningCredentials is null 23 | ? options.SigningCredentials 24 | : options.RefreshTokenSigningCredentialsFactory?.Invoke(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Authorization/Jwt/JsonWebTokenService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.IdentityModel.Tokens; 5 | using System.IdentityModel.Tokens.Jwt; 6 | using System.Security.Claims; 7 | using WeihanLi.Common.Services; 8 | using WeihanLi.Extensions; 9 | using WeihanLi.Web.Authorization.Token; 10 | 11 | namespace WeihanLi.Web.Authorization.Jwt; 12 | 13 | public class JsonWebTokenService : ITokenService 14 | { 15 | private readonly IHttpContextAccessor _httpContextAccessor; 16 | private readonly JwtSecurityTokenHandler _tokenHandler = new(); 17 | private readonly JsonWebTokenOptions _tokenOptions; 18 | 19 | private readonly Lazy 20 | _lazyTokenValidationParameters, 21 | _lazyRefreshTokenValidationParameters; 22 | 23 | public JsonWebTokenService(IHttpContextAccessor httpContextAccessor, IOptions tokenOptions) 24 | { 25 | _httpContextAccessor = httpContextAccessor; 26 | _tokenOptions = tokenOptions.Value; 27 | _lazyTokenValidationParameters = new(() => 28 | _tokenOptions.GetTokenValidationParameters()); 29 | _lazyRefreshTokenValidationParameters = new(() => 30 | _tokenOptions.GetTokenValidationParameters(parameters => 31 | { 32 | parameters.ValidAudience = GetRefreshTokenAudience(); 33 | parameters.IssuerSigningKey = _tokenOptions.RefreshTokenSigningCredentials?.Key; 34 | }) 35 | ); 36 | } 37 | 38 | public virtual Task GenerateToken(params Claim[] claims) 39 | => GenerateTokenInternal(_tokenOptions.EnableRefreshToken, claims); 40 | 41 | public virtual Task ValidateToken(string token) 42 | { 43 | return _tokenHandler.ValidateTokenAsync(token, _lazyTokenValidationParameters.Value); 44 | } 45 | 46 | public virtual async Task RefreshToken(string refreshToken) 47 | { 48 | ArgumentNullException.ThrowIfNull(_httpContextAccessor.HttpContext); 49 | var refreshTokenValidateResult = await _tokenHandler.ValidateTokenAsync(refreshToken, _lazyRefreshTokenValidationParameters.Value); 50 | if (!refreshTokenValidateResult.IsValid || _tokenOptions.RefreshTokenValidator?.Invoke(refreshTokenValidateResult, _httpContextAccessor.HttpContext) == false) 51 | { 52 | throw new InvalidOperationException("Invalid RefreshToken", refreshTokenValidateResult.Exception); 53 | } 54 | var renewRefreshToken = _tokenOptions.RenewRefreshTokenPredicate?.Invoke(refreshTokenValidateResult); 55 | return await GenerateTokenInternal(renewRefreshToken.GetValueOrDefault(), 56 | refreshTokenValidateResult.Claims 57 | .Where(x => x.Key != JwtRegisteredClaimNames.Jti) 58 | .Select(c => new Claim(c.Key, c.Value.ToString() ?? string.Empty)).ToArray() 59 | ); 60 | } 61 | 62 | protected virtual Task GetRefreshToken(Claim[] claims, string jti) 63 | { 64 | var claimList = new List((claims) 65 | .Where(c => c.Type != _tokenOptions.RefreshTokenOwnerClaimType) 66 | .Union([new Claim(_tokenOptions.RefreshTokenOwnerClaimType, jti)]) 67 | ); 68 | 69 | claimList.RemoveAll(c => 70 | JwtInternalClaimTypes.Contains(c.Type) 71 | || c.Type == JwtRegisteredClaimNames.Jti); 72 | var jtiNew = _tokenOptions.JtiGenerator?.Invoke() ?? GuidIdGenerator.Instance.NewId(); 73 | claimList.Add(new(JwtRegisteredClaimNames.Jti, jtiNew)); 74 | var now = DateTimeOffset.UtcNow; 75 | claimList.Add(new Claim(JwtRegisteredClaimNames.Iat, now.ToUnixTimeMilliseconds().ToString(), ClaimValueTypes.Integer64)); 76 | var jwt = new JwtSecurityToken( 77 | issuer: _tokenOptions.Issuer, 78 | audience: GetRefreshTokenAudience(), 79 | claims: claimList, 80 | notBefore: now.UtcDateTime, 81 | expires: now.Add(_tokenOptions.RefreshTokenValidFor).UtcDateTime, 82 | signingCredentials: _tokenOptions.RefreshTokenSigningCredentials); 83 | var encodedJwt = _tokenHandler.WriteToken(jwt); 84 | return encodedJwt.WrapTask(); 85 | } 86 | 87 | private static readonly HashSet JwtInternalClaimTypes = 88 | [ 89 | "iss", 90 | "exp", 91 | "aud", 92 | "nbf", 93 | "iat" 94 | ]; 95 | 96 | private async Task GenerateTokenInternal(bool refreshToken, Claim[]? claims) 97 | { 98 | var now = DateTimeOffset.UtcNow; 99 | var claimList = new List() 100 | { 101 | new (JwtRegisteredClaimNames.Iat, now.ToUnixTimeMilliseconds().ToString(), ClaimValueTypes.Integer64) 102 | }; 103 | if (claims is { Length: > 0 }) 104 | { 105 | claimList.AddRange( 106 | claims.Where(x => !JwtInternalClaimTypes.Contains(x.Type)) 107 | ); 108 | } 109 | var jti = claimList.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Jti)?.Value; 110 | if (jti.IsNullOrEmpty()) 111 | { 112 | jti = _tokenOptions.JtiGenerator?.Invoke() ?? GuidIdGenerator.Instance.NewId(); 113 | claimList.Add(new(JwtRegisteredClaimNames.Jti, jti)); 114 | } 115 | var jwt = new JwtSecurityToken( 116 | issuer: _tokenOptions.Issuer, 117 | audience: _tokenOptions.Audience, 118 | claims: claimList, 119 | notBefore: now.UtcDateTime, 120 | expires: now.Add(_tokenOptions.ValidFor).UtcDateTime, 121 | signingCredentials: _tokenOptions.SigningCredentials); 122 | var encodedJwt = _tokenHandler.WriteToken(jwt); 123 | 124 | var response = refreshToken ? new TokenEntityWithRefreshToken() 125 | { 126 | AccessToken = encodedJwt, 127 | ExpiresIn = (int)_tokenOptions.ValidFor.TotalSeconds, 128 | RefreshToken = await GetRefreshToken(claims ?? [], jti) 129 | } : new TokenEntity() 130 | { 131 | AccessToken = encodedJwt, 132 | ExpiresIn = (int)_tokenOptions.ValidFor.TotalSeconds 133 | }; 134 | return response; 135 | } 136 | 137 | private string GetRefreshTokenAudience() => $"{_tokenOptions.Audience}_RefreshToken"; 138 | } 139 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Authorization/Jwt/JwtBearerOptionsPostSetup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Authentication.JwtBearer; 5 | 6 | namespace WeihanLi.Web.Authorization.Jwt; 7 | 8 | internal sealed class JwtBearerOptionsPostSetup(IOptions options) : 9 | IPostConfigureOptions 10 | { 11 | private readonly IOptions _options = options; 12 | 13 | public void PostConfigure(string? name, JwtBearerOptions options) 14 | { 15 | options.Audience = _options.Value.Audience; 16 | options.ClaimsIssuer = _options.Value.Issuer; 17 | options.TokenValidationParameters = _options.Value.GetTokenValidationParameters(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Authorization/Token/ITokenService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.IdentityModel.Tokens; 5 | using System.Security.Claims; 6 | 7 | namespace WeihanLi.Web.Authorization.Token; 8 | 9 | public interface ITokenService 10 | { 11 | Task GenerateToken(params Claim[] claims); 12 | 13 | Task ValidateToken(string token); 14 | 15 | Task RefreshToken(string refreshToken); 16 | } 17 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Authorization/Token/TokenEntity.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | namespace WeihanLi.Web.Authorization.Token; 5 | 6 | public class TokenEntity 7 | { 8 | public required string AccessToken { get; set; } 9 | public int ExpiresIn { get; set; } 10 | } 11 | 12 | public class TokenEntityWithRefreshToken : TokenEntity 13 | { 14 | public string? RefreshToken { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/DataProtection/DataProtectionBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.DataProtection; 5 | using Microsoft.AspNetCore.Mvc; 6 | using WeihanLi.Web.DataProtection.ParamsProtection; 7 | 8 | namespace WeihanLi.Web.DataProtection; 9 | 10 | public static class DataProtectionBuilderExtensions 11 | { 12 | /// 13 | /// AddParamsProtection 14 | /// 15 | /// dataProtectionBuilder 16 | /// 17 | public static IDataProtectionBuilder AddParamsProtection(this IDataProtectionBuilder builder) 18 | { 19 | ArgumentNullException.ThrowIfNull(builder); 20 | 21 | builder.Services.Configure(action => 22 | { 23 | action.Filters.Add(); 24 | action.Filters.Add(); 25 | }); 26 | 27 | return builder; 28 | } 29 | 30 | /// 31 | /// AddParamsProtection 32 | /// 33 | /// dataProtectionBuilder 34 | /// options config action 35 | /// 36 | public static IDataProtectionBuilder AddParamsProtection(this IDataProtectionBuilder builder, Action optionsAction) 37 | { 38 | ArgumentNullException.ThrowIfNull(builder); 39 | ArgumentNullException.ThrowIfNull(optionsAction); 40 | 41 | builder.Services.Configure(optionsAction); 42 | builder.AddParamsProtection(); 43 | return builder; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/DataProtection/ParamsProtection/ParamsProtectionHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.DataProtection; 5 | using Newtonsoft.Json.Linq; 6 | using System.Diagnostics; 7 | using System.Diagnostics.CodeAnalysis; 8 | using System.Security.Cryptography; 9 | 10 | namespace WeihanLi.Web.DataProtection.ParamsProtection; 11 | 12 | internal static class ParamsProtectionHelper 13 | { 14 | public const string DefaultPurpose = "ParamsProtector"; 15 | 16 | private static bool IsParamNeedProtect(this ParamsProtectionOptions option, string propName, string? value) 17 | { 18 | if (option.ProtectParams.Any(p => p.Equals(propName, StringComparison.OrdinalIgnoreCase))) 19 | { 20 | return (!option.ParamValueProtectFuncEnabled || option.ParamValueNeedProtectFunc(value)); 21 | } 22 | return false; 23 | } 24 | 25 | private static bool IsParamNeedUnprotect(this ParamsProtectionOptions option, string propName, string? value) 26 | { 27 | if (option.ProtectParams.Any(p => p.Equals(propName, StringComparison.OrdinalIgnoreCase))) 28 | { 29 | return !option.ParamValueProtectFuncEnabled || !option.ParamValueNeedProtectFunc(value); 30 | } 31 | return false; 32 | } 33 | 34 | private static void ProtectParams(JToken token, ITimeLimitedDataProtector protector, ParamsProtectionOptions option) 35 | { 36 | if (token is JArray array) 37 | { 38 | foreach (var j in array) 39 | { 40 | if (array.Parent is JProperty property && j is JValue val) 41 | { 42 | var strJ = val.Value?.ToString(); 43 | if (option.IsParamNeedProtect(property.Name, strJ)) 44 | { 45 | val.Value = protector.Protect(strJ!, TimeSpan.FromMinutes(option.ExpiresIn.GetValueOrDefault(10))); 46 | } 47 | } 48 | else 49 | { 50 | ProtectParams(j, protector, option); 51 | } 52 | } 53 | } 54 | else if (token is JObject obj) 55 | { 56 | foreach (var property in obj.Children()) 57 | { 58 | var val = property.Value.ToString(); 59 | if (option.IsParamNeedProtect(property.Name, val)) 60 | { 61 | property.Value = protector.Protect(val, TimeSpan.FromMinutes(option.ExpiresIn.GetValueOrDefault(10))); 62 | } 63 | else 64 | { 65 | if (property.Value.HasValues) 66 | { 67 | ProtectParams(property.Value, protector, option); 68 | } 69 | } 70 | } 71 | } 72 | } 73 | 74 | public static bool TryGetUnprotectedValue(this IDataProtector protector, ParamsProtectionOptions option, 75 | string? value, [MaybeNullWhen(false)] out string unprotectedValue) 76 | { 77 | unprotectedValue = value; 78 | if (string.IsNullOrEmpty(value)) 79 | return false; 80 | 81 | if (option is { AllowUnprotectedParams: true, ParamValueProtectFuncEnabled: true } && option.ParamValueNeedProtectFunc(value) 82 | ) 83 | { 84 | unprotectedValue = value; 85 | } 86 | else 87 | { 88 | try 89 | { 90 | unprotectedValue = protector.Unprotect(value); 91 | } 92 | catch (Exception e) 93 | { 94 | Debug.WriteLine(e, $"Error in unprotect value:{value}"); 95 | unprotectedValue = value; 96 | if (option.AllowUnprotectedParams && e is CryptographicException && !e.Message.Contains("expired")) 97 | { 98 | return true; 99 | } 100 | return false; 101 | } 102 | } 103 | return true; 104 | } 105 | 106 | public static void ProtectParams(JToken token, IDataProtector protector, ParamsProtectionOptions option) 107 | { 108 | if (option is { Enabled: true, ProtectParams.Length: > 0 }) 109 | { 110 | if (protector is ITimeLimitedDataProtector timeLimitedDataProtector) 111 | { 112 | ProtectParams(token, timeLimitedDataProtector, option); 113 | return; 114 | } 115 | if (token is JArray array) 116 | { 117 | foreach (var j in array) 118 | { 119 | if (array.Parent is JProperty property && j is JValue val) 120 | { 121 | var strJ = val.Value?.ToString(); 122 | if (option.IsParamNeedProtect(property.Name, strJ)) 123 | { 124 | val.Value = protector.Protect(strJ!); 125 | } 126 | } 127 | else 128 | { 129 | ProtectParams(j, protector, option); 130 | } 131 | } 132 | } 133 | else if (token is JObject obj) 134 | { 135 | foreach (var property in obj.Children()) 136 | { 137 | var val = property.Value.ToString(); 138 | if (option.IsParamNeedProtect(property.Name, val)) 139 | { 140 | property.Value = protector.Protect(val); 141 | } 142 | else 143 | { 144 | if (property.Value.HasValues) 145 | { 146 | ProtectParams(property.Value, protector, option); 147 | } 148 | } 149 | } 150 | } 151 | } 152 | } 153 | 154 | public static void UnProtectParams(JToken token, IDataProtector protector, ParamsProtectionOptions option) 155 | { 156 | if (option is { Enabled: true, ProtectParams.Length: > 0 }) 157 | { 158 | if (token is JArray array) 159 | { 160 | foreach (var j in array) 161 | { 162 | if (j is JValue val) 163 | { 164 | var strJ = val.Value?.ToString(); 165 | if (array.Parent is JProperty property && option.IsParamNeedUnprotect(property.Name, strJ)) 166 | { 167 | try 168 | { 169 | val.Value = protector.Unprotect(strJ!); 170 | } 171 | catch (Exception e) 172 | { 173 | Debug.WriteLine(e); 174 | if (option.AllowUnprotectedParams && e is CryptographicException && !e.Message.Contains("expired")) 175 | { 176 | val.Value = strJ; 177 | } 178 | else 179 | { 180 | throw; 181 | } 182 | } 183 | } 184 | } 185 | else 186 | { 187 | UnProtectParams(j, protector, option); 188 | } 189 | } 190 | } 191 | else if (token is JObject obj) 192 | { 193 | foreach (var property in obj.Children()) 194 | { 195 | if (property.Value is JArray) 196 | { 197 | UnProtectParams(property.Value, protector, option); 198 | } 199 | else 200 | { 201 | var val = property.Value.ToString(); 202 | if (option.IsParamNeedUnprotect(property.Name, val)) 203 | { 204 | try 205 | { 206 | property.Value = protector.Unprotect(val); 207 | } 208 | catch (Exception e) 209 | { 210 | Debug.WriteLine(e); 211 | if (option.AllowUnprotectedParams && e is CryptographicException && !e.Message.Contains("expired")) 212 | { 213 | property.Value = val; 214 | } 215 | else 216 | { 217 | throw; 218 | } 219 | } 220 | } 221 | else 222 | { 223 | if (property.Value.HasValues) 224 | { 225 | UnProtectParams(property.Value, protector, option); 226 | } 227 | } 228 | } 229 | } 230 | } 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/DataProtection/ParamsProtection/ParamsProtectionOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Mvc; 5 | using System.Linq.Expressions; 6 | using WeihanLi.Extensions; 7 | 8 | namespace WeihanLi.Web.DataProtection.ParamsProtection; 9 | 10 | public sealed class ParamsProtectionOptions 11 | { 12 | private string[] _protectParams = []; 13 | 14 | /// 15 | /// ProtectorPurpose 16 | /// 17 | public string? ProtectorPurpose { get; set; } = "ParamsProtection"; 18 | 19 | /// 20 | /// ExpiresIn, minutes 21 | /// 22 | public int? ExpiresIn { get; set; } 23 | 24 | /// 25 | /// Enabled for paramsProtection 26 | /// 27 | public bool Enabled { get; set; } = true; 28 | 29 | /// 30 | /// Allow unprotected params 31 | /// 32 | public bool AllowUnprotectedParams { get; set; } 33 | 34 | /// 35 | /// Invalid request response http status code 36 | /// refer to https://restfulapi.net/http-status-codes/ 37 | /// 38 | public int InvalidRequestStatusCode { get; set; } = 412; 39 | 40 | /// 41 | /// the params to protect 42 | /// 43 | public string[] ProtectParams 44 | { 45 | get => _protectParams; 46 | set 47 | { 48 | // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract 49 | if (value is not null) 50 | { 51 | _protectParams = value; 52 | } 53 | } 54 | } 55 | 56 | /// 57 | /// whether the NeedProtectFunc is enabled 58 | /// 59 | public bool ParamValueProtectFuncEnabled { get; set; } 60 | 61 | /// 62 | /// whether the parameter should be protected 63 | /// 64 | public Func ParamValueNeedProtectFunc { get; set; } = str => long.TryParse(str, out _); 65 | 66 | /// 67 | /// whether the response should be protected 68 | /// 69 | internal IDictionary NeedProtectResponseValues { get; } = new Dictionary() 70 | { 71 | { typeof(ObjectResult), "Value"} 72 | }; 73 | 74 | /// 75 | /// Add type and value ToProtectValues 76 | /// 77 | /// TResult 78 | /// the value of the type to protect 79 | public void AddProtectValue(Expression> valueExpression) where TResult : class, IActionResult 80 | { 81 | NeedProtectResponseValues[typeof(TResult)] = valueExpression.GetMemberInfo().Name; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/DataProtection/ParamsProtection/ParamsProtectionResourceFilter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.DataProtection; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.Filters; 7 | using Microsoft.Extensions.Logging; 8 | using Microsoft.Extensions.Primitives; 9 | using Newtonsoft.Json.Linq; 10 | using System.Text; 11 | using WeihanLi.Common.Helpers; 12 | using WeihanLi.Extensions; 13 | 14 | namespace WeihanLi.Web.DataProtection.ParamsProtection; 15 | 16 | public sealed class ParamsProtectionResourceFilter : IResourceFilter 17 | { 18 | private static readonly Lazy XmlDataSerializer = new(() => new XmlDataSerializer()); 19 | private readonly IDataProtector _protector; 20 | private readonly ParamsProtectionOptions _option; 21 | 22 | private readonly ILogger _logger; 23 | 24 | public ParamsProtectionResourceFilter(IDataProtectionProvider protectionProvider, ILogger logger, IOptions options) 25 | { 26 | _option = options.Value; 27 | _protector = protectionProvider.CreateProtector(_option.ProtectorPurpose ?? ParamsProtectionHelper.DefaultPurpose); 28 | if (_option.ExpiresIn.GetValueOrDefault(0) > 0) 29 | { 30 | _protector = _protector.ToTimeLimitedDataProtector(); 31 | } 32 | 33 | _logger = logger; 34 | } 35 | 36 | public void OnResourceExecuting(ResourceExecutingContext context) 37 | { 38 | if (_option is { Enabled: true, ProtectParams.Length: > 0 }) 39 | { 40 | var request = context.HttpContext.Request; 41 | 42 | // QueryString 43 | if (request.Query.Count > 0) 44 | { 45 | var queryDic = request.Query.ToDictionary(query => query.Key, query => query.Value); 46 | foreach (var param in _option.ProtectParams) 47 | { 48 | if (queryDic.ContainsKey(param)) 49 | { 50 | var values = new List(queryDic[param].Count); 51 | for (var i = 0; i < queryDic[param].Count; i++) 52 | { 53 | if (_protector.TryGetUnprotectedValue(_option, queryDic[param][i]!, out var val)) 54 | { 55 | values.Add(val); 56 | } 57 | else 58 | { 59 | _logger.LogWarning($"Error in unprotect query value: param:{param}"); 60 | context.Result = new StatusCodeResult(_option.InvalidRequestStatusCode); 61 | 62 | return; 63 | } 64 | } 65 | queryDic[param] = new StringValues(values.ToArray()); 66 | } 67 | context.HttpContext.Request.Query = new QueryCollection(queryDic); 68 | } 69 | } 70 | // route value 71 | if (context.RouteData.Values.Count > 0) 72 | { 73 | foreach (var param in _option.ProtectParams) 74 | { 75 | if (context.RouteData.Values.ContainsKey(param)) 76 | { 77 | if (_protector.TryGetUnprotectedValue(_option, context.RouteData.Values[param]?.ToString(), out var val)) 78 | { 79 | context.RouteData.Values[param] = val; 80 | } 81 | else 82 | { 83 | _logger.LogWarning($"Error in un-protect routeValue:{param}"); 84 | 85 | context.Result = new StatusCodeResult(_option.InvalidRequestStatusCode); 86 | 87 | return; 88 | } 89 | } 90 | } 91 | } 92 | 93 | if (request.Method.EqualsIgnoreCase("POST") || request.Method.EqualsIgnoreCase("PUT")) 94 | { 95 | var contentType = request.ContentType ?? string.Empty; 96 | if (contentType.Contains("json")) 97 | { 98 | using var reader = new StreamReader(request.Body, Encoding.UTF8); 99 | var content = reader.ReadToEnd(); 100 | var obj = content.JsonToObject(); 101 | try 102 | { 103 | ParamsProtectionHelper.UnProtectParams(obj, _protector, _option); 104 | } 105 | catch (Exception e) 106 | { 107 | _logger.LogWarning(e, "Error in unprotect request body"); 108 | 109 | context.Result = new StatusCodeResult(_option.InvalidRequestStatusCode); 110 | 111 | return; 112 | } 113 | 114 | context.HttpContext.Request.Body = obj.ToJson().GetBytes().ToMemoryStream(); 115 | } // json body 116 | else if (contentType.Contains("xml")) 117 | { 118 | // TODO: need test 119 | var obj = XmlDataSerializer.Value.Deserialize(request.Body.ToByteArray()); 120 | try 121 | { 122 | ParamsProtectionHelper.UnProtectParams(obj, _protector, _option); 123 | } 124 | catch (Exception e) 125 | { 126 | _logger.LogWarning(e, "Error in unprotect request body"); 127 | 128 | context.Result = new StatusCodeResult(_option.InvalidRequestStatusCode); 129 | 130 | return; 131 | } 132 | context.HttpContext.Request.Body = XmlDataSerializer.Value.Serialize(obj).ToMemoryStream(); 133 | } // xml body 134 | 135 | // form data 136 | if (request is { HasFormContentType: true, Form.Count: > 0 }) 137 | { 138 | var formDic = request.Form.ToDictionary(p => p.Key, p => p.Value); 139 | foreach (var param in _option.ProtectParams) 140 | { 141 | if (formDic.TryGetValue(param, out var values)) 142 | { 143 | var vals = new List(); 144 | foreach (var item in values) 145 | { 146 | if (_protector.TryGetUnprotectedValue(_option, item, out var val)) 147 | { 148 | vals.Add(val); 149 | } 150 | else 151 | { 152 | _logger.LogWarning($"Error in unprotect form data: param:{param}"); 153 | context.Result = new StatusCodeResult(_option.InvalidRequestStatusCode); 154 | 155 | return; 156 | } 157 | } 158 | formDic[param] = new StringValues(vals.ToArray()); 159 | } 160 | } 161 | request.Form = new FormCollection(formDic); 162 | } 163 | } 164 | } 165 | } 166 | 167 | public void OnResourceExecuted(ResourceExecutedContext context) 168 | { 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/DataProtection/ParamsProtection/ParamsProtectionResultFilter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.DataProtection; 5 | using Microsoft.AspNetCore.Mvc.Filters; 6 | using Microsoft.Extensions.Logging; 7 | using Newtonsoft.Json.Linq; 8 | using WeihanLi.Common; 9 | using WeihanLi.Extensions; 10 | 11 | namespace WeihanLi.Web.DataProtection.ParamsProtection; 12 | 13 | public sealed class ParamsProtectionResultFilter : IResultFilter 14 | { 15 | private readonly IDataProtector _protector; 16 | private readonly ParamsProtectionOptions _option; 17 | private readonly ILogger _logger; 18 | 19 | public ParamsProtectionResultFilter(IDataProtectionProvider protectionProvider, IOptions options, ILogger logger) 20 | { 21 | _logger = logger; 22 | _option = options.Value; 23 | 24 | _protector = protectionProvider.CreateProtector(_option.ProtectorPurpose ?? ParamsProtectionHelper.DefaultPurpose); 25 | 26 | if (_option.ExpiresIn.GetValueOrDefault(0) > 0) 27 | { 28 | _protector = _protector.ToTimeLimitedDataProtector(); 29 | } 30 | } 31 | 32 | public void OnResultExecuting(ResultExecutingContext context) 33 | { 34 | if (_option is { Enabled: true, ProtectParams.Length: > 0 }) 35 | { 36 | foreach (var pair in _option.NeedProtectResponseValues) 37 | { 38 | if (pair.Key.IsInstanceOfType(context.Result)) 39 | { 40 | var prop = CacheUtil.GetTypeProperties(pair.Key).FirstOrDefault(p => p.Name == pair.Value); 41 | var val = prop?.GetValueGetter()?.Invoke(context.Result); 42 | if (val is not null) 43 | { 44 | _logger.LogDebug($"ParamsProtector is protecting {pair.Key.FullName} Value"); 45 | 46 | var obj = JToken.FromObject(val); 47 | ParamsProtectionHelper.ProtectParams(obj, _protector, _option); 48 | prop!.GetValueSetter()?.Invoke(context.Result, obj); 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | public void OnResultExecuted(ResultExecutedContext context) 56 | { 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Extensions/DependenceResolverExtension.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using WeihanLi.Common; 5 | 6 | namespace WeihanLi.Web.Extensions; 7 | 8 | public static class DependenceResolverExtension 9 | { 10 | /// 11 | /// try get service from HttpContext.RequestServices 12 | /// 13 | /// TService 14 | /// dependencyResolver 15 | /// service instance 16 | public static TService ResolveCurrentService(this IDependencyResolver dependencyResolver) 17 | where TService : class 18 | { 19 | var contextAccessor = dependencyResolver.GetRequiredService(); 20 | return Guard.NotNull(contextAccessor.HttpContext).RequestServices.GetRequiredService(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Extensions/EndpointExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using WeihanLi.Common.Helpers; 5 | using WeihanLi.Web.Middleware; 6 | 7 | namespace WeihanLi.Web.Extensions; 8 | 9 | public static class EndpointExtensions 10 | { 11 | public static IEndpointConventionBuilder MapRuntimeInfo(this IEndpointRouteBuilder endpointRouteBuilder, string path = "/runtime-info") 12 | { 13 | ArgumentNullException.ThrowIfNull(endpointRouteBuilder); 14 | return endpointRouteBuilder.MapGet(path, () => ApplicationHelper.RuntimeInfo); 15 | } 16 | 17 | public static IEndpointConventionBuilder MapConfigInspector(this IEndpointRouteBuilder endpointRouteBuilder, 18 | string path = "/config-inspector", 19 | Action? optionsConfigure = null 20 | ) 21 | { 22 | ArgumentNullException.ThrowIfNull(endpointRouteBuilder); 23 | 24 | if (optionsConfigure is not null) 25 | { 26 | var options = endpointRouteBuilder.ServiceProvider.GetRequiredService>(); 27 | optionsConfigure(options.Value); 28 | } 29 | 30 | return endpointRouteBuilder.MapGet($"{path}/{{configKey?}}", async (context) => 31 | { 32 | var options = context.RequestServices.GetRequiredService>(); 33 | await ConfigInspectorMiddleware.InvokeAsync(context, options); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Extensions/FluentAspectServiceProviderFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.Extensions.Hosting; 5 | using System.Linq.Expressions; 6 | using WeihanLi.Common.Aspect; 7 | using WeihanLi.Extensions; 8 | 9 | namespace WeihanLi.Web.Extensions; 10 | 11 | internal sealed class FluentAspectsServiceProviderFactory( 12 | Action optionsAction, 13 | Action? aspectBuildAction, 14 | Expression> ignoreTypesPredict) 15 | : IServiceProviderFactory 16 | { 17 | public IServiceCollection CreateBuilder(IServiceCollection services) 18 | { 19 | return services; 20 | } 21 | 22 | public IServiceProvider CreateServiceProvider(IServiceCollection containerBuilder) 23 | { 24 | return containerBuilder.BuildFluentAspectsProvider(optionsAction, aspectBuildAction, ignoreTypesPredict); 25 | } 26 | } 27 | 28 | public static class FluentAspectServiceProviderFactoryExtensions 29 | { 30 | public static IHostBuilder UseFluentAspectsServiceProviderFactory(this IHostBuilder hostBuilder, 31 | Action optionsAction, 32 | Action? aspectBuildAction = null, 33 | Expression>? ignoreTypesPredict = null) 34 | { 35 | if (ignoreTypesPredict == null) 36 | { 37 | ignoreTypesPredict = t => 38 | t.HasNamespace() 39 | && (t.Namespace!.StartsWith("Microsoft.") == true 40 | || t.Namespace.StartsWith("System.") == true) 41 | ; 42 | } 43 | hostBuilder.UseServiceProviderFactory( 44 | new FluentAspectsServiceProviderFactory(optionsAction, aspectBuildAction, ignoreTypesPredict) 45 | ); 46 | return hostBuilder; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Extensions/HealthCheckExtension.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace WeihanLi.Web.Extensions; 7 | 8 | public static class HealthCheckExtensions 9 | { 10 | public static IApplicationBuilder UseHealthCheck(this IApplicationBuilder applicationBuilder) 11 | { 12 | return UseHealthCheck(applicationBuilder, new PathString("/api/health")); 13 | } 14 | 15 | public static IApplicationBuilder UseHealthCheck(this IApplicationBuilder applicationBuilder, string path) 16 | { 17 | return UseHealthCheck(applicationBuilder, new PathString(path)); 18 | } 19 | 20 | public static IApplicationBuilder UseHealthCheck(this IApplicationBuilder applicationBuilder, PathString path) 21 | { 22 | static Task DefaultHealthCheck(HttpContext context, Func next) 23 | { 24 | context.Response.StatusCode = 200; 25 | return context.Response.WriteAsync("healthy"); 26 | } 27 | applicationBuilder.Map(path, builder => builder.Use(DefaultHealthCheck)); 28 | return applicationBuilder; 29 | } 30 | 31 | public static IApplicationBuilder UseHealthCheck(this IApplicationBuilder applicationBuilder, string path, Func checkFunc) 32 | { 33 | return UseHealthCheck(applicationBuilder, new PathString(path), serviceProvider => Task.FromResult(checkFunc(serviceProvider))); 34 | } 35 | 36 | public static IApplicationBuilder UseHealthCheck(this IApplicationBuilder applicationBuilder, string path, 37 | Func> checkFunc) 38 | { 39 | return UseHealthCheck(applicationBuilder, new PathString(path), checkFunc); 40 | } 41 | 42 | public static IApplicationBuilder UseHealthCheck(this IApplicationBuilder applicationBuilder, PathString path, Func? checkFunc) 43 | { 44 | if (checkFunc == null) 45 | { 46 | checkFunc = _ => true; 47 | } 48 | return UseHealthCheck(applicationBuilder, path, serviceProvider => Task.FromResult(checkFunc(serviceProvider))); 49 | } 50 | 51 | public static IApplicationBuilder UseHealthCheck(this IApplicationBuilder applicationBuilder, PathString path, Func>? checkFunc) 52 | { 53 | if (checkFunc == null) 54 | { 55 | checkFunc = _ => Task.FromResult(true); 56 | } 57 | 58 | async Task CheckFunc(HttpContext context, Func next) 59 | { 60 | try 61 | { 62 | var healthy = await checkFunc.Invoke(context.RequestServices); 63 | if (healthy) 64 | { 65 | context.Response.StatusCode = StatusCodes.Status200OK; 66 | await context.Response.WriteAsync("healthy"); 67 | } 68 | else 69 | { 70 | context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; 71 | await context.Response.WriteAsync("unhealthy"); 72 | } 73 | } 74 | catch (Exception ex) 75 | { 76 | context.RequestServices.GetRequiredService() 77 | .CreateLogger(typeof(HealthCheckExtensions)).LogError(ex, "HealthCheck Exception"); 78 | context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; 79 | await context.Response.WriteAsync("unhealthy"); 80 | } 81 | } 82 | applicationBuilder.Map(path, builder => builder.Use(CheckFunc)); 83 | return applicationBuilder; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Extensions/HttpContextExtension.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using System.IdentityModel.Tokens.Jwt; 5 | using System.Security.Claims; 6 | using WeihanLi.Extensions; 7 | 8 | namespace WeihanLi.Web.Extensions; 9 | 10 | public static class HttpContextExtension 11 | { 12 | /// 13 | /// GetUserIP 14 | /// 15 | /// httpContext 16 | /// realIPHeader, default `X-Forwarded-For` 17 | /// user ip 18 | // ReSharper disable InconsistentNaming 19 | public static string? GetUserIP(this HttpContext httpContext, string realIPHeader = "X-Forwarded-For") 20 | { 21 | ArgumentNullException.ThrowIfNull(httpContext); 22 | 23 | return httpContext.Request.Headers.TryGetValue(realIPHeader, out var ip) 24 | ? ip.ToString() 25 | : httpContext.Connection.RemoteIpAddress?.MapToIPv4().ToString(); 26 | } 27 | 28 | /// 29 | /// GetUserId from claims, get from `ClaimTypes.NameIdentifier/`nameid`/`sub` by default 30 | /// 31 | /// principal 32 | /// 33 | public static string? GetUserId(this ClaimsPrincipal principal) 34 | { 35 | var userId = GetUserId(principal, ClaimTypes.NameIdentifier); 36 | if (!string.IsNullOrEmpty(userId)) 37 | { 38 | return userId; 39 | } 40 | userId = GetUserId(principal, JwtRegisteredClaimNames.NameId); 41 | if (!string.IsNullOrEmpty(userId)) 42 | { 43 | return userId; 44 | } 45 | return GetUserId(principal, JwtRegisteredClaimNames.Sub); 46 | } 47 | 48 | /// 49 | /// GetUserId from claims, get from ClaimTypes.NameIdentifier/`nameid`/`sub` by default 50 | /// 51 | /// userId type 52 | /// principal 53 | /// 54 | public static T GetUserId(this ClaimsPrincipal principal) => GetUserId(principal).ToOrDefault()!; 55 | 56 | public static T GetUserId(this ClaimsPrincipal principal, string claimType) 57 | => GetUserId(principal, claimType).ToOrDefault()!; 58 | 59 | public static string? GetUserId(this ClaimsPrincipal principal, string claimType) 60 | => principal.FindFirst(claimType)?.Value; 61 | } 62 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Extensions/HttpContextTenantProviderExtension.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.Extensions.DependencyInjection.Extensions; 5 | using WeihanLi.Common; 6 | using WeihanLi.Common.Services; 7 | using WeihanLi.Web.Services; 8 | 9 | namespace WeihanLi.Web.Extensions; 10 | 11 | public static class HttpContextTenantProviderExtension 12 | { 13 | public static IServiceCollection AddHttpContextTenantProvider(this IServiceCollection serviceCollection) 14 | { 15 | Guard.NotNull(serviceCollection); 16 | serviceCollection.TryAddSingleton(); 17 | serviceCollection.TryAddSingleton(); 18 | return serviceCollection; 19 | } 20 | 21 | public static IServiceCollection AddHttpContextTenantProvider(this IServiceCollection serviceCollection, Action optionsAction) 22 | { 23 | Guard.NotNull(serviceCollection); 24 | Guard.NotNull(optionsAction); 25 | 26 | serviceCollection.Configure(optionsAction); 27 | return serviceCollection.AddHttpContextTenantProvider(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Extensions/HttpContextUserIdProviderExtension.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.Extensions.DependencyInjection.Extensions; 5 | using WeihanLi.Common; 6 | using WeihanLi.Common.Services; 7 | using WeihanLi.Web.Services; 8 | 9 | namespace WeihanLi.Web.Extensions; 10 | 11 | public static class HttpContextUserIdProviderExtension 12 | { 13 | public static IServiceCollection AddHttpContextUserIdProvider(this IServiceCollection serviceCollection) 14 | { 15 | Guard.NotNull(serviceCollection); 16 | serviceCollection.TryAddSingleton(); 17 | serviceCollection.TryAddSingleton(); 18 | return serviceCollection; 19 | } 20 | 21 | public static IServiceCollection AddHttpContextUserIdProvider(this IServiceCollection serviceCollection, Action optionsAction) 22 | { 23 | Guard.NotNull(serviceCollection); 24 | Guard.NotNull(optionsAction); 25 | 26 | serviceCollection.Configure(optionsAction); 27 | return serviceCollection.AddHttpContextUserIdProvider(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Extensions/MiddlewareExtension.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using WeihanLi.Web.Middleware; 5 | 6 | namespace WeihanLi.Web.Extensions; 7 | 8 | public static class MiddlewareExtension 9 | { 10 | /// 11 | /// UseCustomExceptionHandler 12 | /// 13 | /// applicationBuilder 14 | /// 15 | public static IApplicationBuilder UseCustomExceptionHandler(this IApplicationBuilder applicationBuilder) 16 | { 17 | applicationBuilder.UseMiddleware(); 18 | return applicationBuilder; 19 | } 20 | 21 | /// 22 | /// Use middleware if feature is enabled 23 | /// 24 | public static IApplicationBuilder UseIfFeatureEnabled(this IApplicationBuilder app, Func middleware, string featureFlagName, bool defaultValue = false) 25 | { 26 | var configuration = app.ApplicationServices.GetRequiredService(); 27 | if (configuration.IsFeatureEnabled(featureFlagName, defaultValue)) 28 | { 29 | app.Use(middleware); 30 | } 31 | return app; 32 | } 33 | 34 | /// 35 | /// Use middleware if feature is enabled 36 | /// 37 | public static IApplicationBuilder UseIfFeatureEnabled(this IApplicationBuilder app, Func middleware, string featureFlagName, bool defaultValue = false) 38 | { 39 | var configuration = app.ApplicationServices.GetRequiredService(); 40 | if (configuration.IsFeatureEnabled(featureFlagName, defaultValue)) 41 | { 42 | app.Use(middleware); 43 | } 44 | return app; 45 | } 46 | 47 | /// 48 | /// Use middleware if feature is enabled 49 | /// 50 | public static IApplicationBuilder UseIfFeatureEnabled(this IApplicationBuilder app, Func, Task> middleware, string featureFlagName, bool defaultValue = false) 51 | { 52 | var configuration = app.ApplicationServices.GetRequiredService(); 53 | if (configuration.IsFeatureEnabled(featureFlagName, defaultValue)) 54 | { 55 | app.Use(middleware); 56 | } 57 | return app; 58 | } 59 | 60 | public static IApplicationBuilder UseIfFeatureEnabled(this IApplicationBuilder app, string featureFlagName, bool defaultValue = false) 61 | { 62 | var configuration = app.ApplicationServices.GetRequiredService(); 63 | if (configuration.IsFeatureEnabled(featureFlagName, defaultValue)) 64 | { 65 | app.UseMiddleware(); 66 | } 67 | return app; 68 | } 69 | 70 | /// 71 | /// Use middleware when feature is enabled, based on UseWhen 72 | /// 73 | public static IApplicationBuilder UseWhenFeatureEnabled( 74 | this IApplicationBuilder app, 75 | Action configure, 76 | string featureFlagName, 77 | bool defaultValue = false) 78 | { 79 | return app.UseWhen(context => context.RequestServices.GetRequiredService() 80 | .IsFeatureEnabled(featureFlagName, defaultValue), configure); 81 | } 82 | 83 | /// 84 | /// Use ConfigInspector to inspect config when necessary 85 | /// 86 | internal static IApplicationBuilder UseConfigInspector(this IApplicationBuilder app, 87 | Action? optionsConfigure = null) 88 | { 89 | ArgumentNullException.ThrowIfNull(app); 90 | 91 | return app.UseMiddleware(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Extensions/ResultExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Mvc; 5 | using System.Net; 6 | using WeihanLi.Common.Models; 7 | 8 | namespace WeihanLi.Web.Extensions; 9 | 10 | public static class ResultModelExtensions 11 | { 12 | public static IActionResult GetOkResult(this T result) 13 | { 14 | return result is null 15 | ? new OkResult() 16 | : new OkObjectResult(result); 17 | } 18 | 19 | public static IActionResult GetRestResult(this T result, ResultStatus status = ResultStatus.Success) 20 | { 21 | if (result is null) 22 | return new NoContentResult(); 23 | 24 | return status switch 25 | { 26 | ResultStatus.BadRequest => new BadRequestObjectResult(result), 27 | ResultStatus.NotFound => new NotFoundObjectResult(result), 28 | ResultStatus.MethodNotAllowed => new ObjectResult(result) 29 | { 30 | StatusCode = (int)HttpStatusCode.MethodNotAllowed 31 | }, 32 | ResultStatus.Unauthorized => new ObjectResult(result) { StatusCode = (int)HttpStatusCode.Unauthorized }, 33 | ResultStatus.Forbidden => new ObjectResult(result) { StatusCode = (int)HttpStatusCode.Forbidden }, 34 | _ => new OkObjectResult(result) 35 | }; 36 | } 37 | 38 | public static IActionResult GetRestResult(this ResultStatus status) 39 | { 40 | return status switch 41 | { 42 | ResultStatus.BadRequest => new BadRequestResult(), 43 | ResultStatus.NotFound => new NotFoundResult(), 44 | ResultStatus.MethodNotAllowed => new StatusCodeResult((int)HttpStatusCode.MethodNotAllowed), 45 | ResultStatus.Unauthorized => new StatusCodeResult((int)HttpStatusCode.Unauthorized), 46 | ResultStatus.Forbidden => new StatusCodeResult((int)HttpStatusCode.Forbidden), 47 | ResultStatus.Success => new OkResult(), 48 | _ => new StatusCodeResult((int)status) 49 | }; 50 | } 51 | 52 | public static IActionResult GetRestResult(this Result result) 53 | { 54 | return GetRestResult(result, result.Status); 55 | } 56 | 57 | public static IActionResult GetRestResult(this Result result) 58 | { 59 | return GetRestResult(result, result.Status); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Filters/ApiResultFilter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.Filters; 6 | using WeihanLi.Common.Models; 7 | 8 | namespace WeihanLi.Web.Filters; 9 | 10 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] 11 | public sealed class ApiResultFilter : Attribute 12 | , IResultFilter, IExceptionFilter 13 | , IEndpointFilter 14 | 15 | { 16 | public void OnResultExecuting(ResultExecutingContext context) 17 | { 18 | if (context.Result is ObjectResult { Value: not Result } objectResult) 19 | { 20 | var result = new Result() 21 | { 22 | Data = objectResult.Value, 23 | Status = HttpStatusCode2ResultStatus(objectResult.StatusCode) 24 | }; 25 | objectResult.Value = result; 26 | } 27 | } 28 | 29 | public void OnResultExecuted(ResultExecutedContext context) 30 | { 31 | } 32 | 33 | public void OnException(ExceptionContext context) 34 | { 35 | var result = Result.Fail(context.Exception.ToString(), ResultStatus.InternalError); 36 | context.Result = new ObjectResult(result) { StatusCode = 500 }; 37 | } 38 | 39 | public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) 40 | { 41 | try 42 | { 43 | var result = await next(context); 44 | if (result is Result or ObjectResult { Value: Result } or IValueHttpResult { Value: Result }) 45 | { 46 | return result; 47 | } 48 | 49 | if (result is ObjectResult { Value: not Result } objectResult) 50 | { 51 | return new Result() 52 | { 53 | Data = objectResult.Value, 54 | Status = HttpStatusCode2ResultStatus(objectResult.StatusCode) 55 | }; 56 | } 57 | 58 | if (result is IValueHttpResult { Value: not Result } valueHttpResult) 59 | { 60 | var status = result is IStatusCodeHttpResult statusCodeHttpResult 61 | ? HttpStatusCode2ResultStatus(statusCodeHttpResult.StatusCode) 62 | : HttpStatusCode2ResultStatus(200); 63 | return new Result() { Data = valueHttpResult.Value, Status = status }; 64 | } 65 | 66 | return new Result() 67 | { 68 | Data = result, 69 | Status = HttpStatusCode2ResultStatus(context.HttpContext.Response.StatusCode) 70 | }; 71 | } 72 | catch (Exception ex) 73 | { 74 | return Result.Fail(ex.ToString(), ResultStatus.InternalError); 75 | } 76 | } 77 | 78 | private static ResultStatus HttpStatusCode2ResultStatus(int? statusCode) 79 | { 80 | statusCode ??= 200; 81 | var status = ResultStatus.Success; 82 | if (Enum.IsDefined(typeof(ResultStatus), statusCode.Value)) 83 | { 84 | status = (ResultStatus)statusCode.Value; 85 | } 86 | 87 | if (status == ResultStatus.None) 88 | { 89 | status = ResultStatus.Success; 90 | } 91 | 92 | return status; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Filters/AuthorizationFilterAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Mvc.Filters; 5 | using WeihanLi.Common; 6 | 7 | namespace WeihanLi.Web.Filters; 8 | 9 | public abstract class AuthorizationFilterAttribute : Attribute, IAuthorizationFilter, IAsyncAuthorizationFilter 10 | { 11 | public virtual void OnAuthorization(AuthorizationFilterContext context) 12 | { 13 | } 14 | 15 | public virtual Task OnAuthorizationAsync(AuthorizationFilterContext context) 16 | { 17 | Guard.NotNull(context); 18 | OnAuthorization(context); 19 | return Task.CompletedTask; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Filters/ConditionalFilter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.Filters; 6 | 7 | namespace WeihanLi.Web.Filters; 8 | 9 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)] 10 | public class ConditionalFilter : Attribute, IAsyncResourceFilter, IEndpointFilter 11 | 12 | { 13 | public Func ConditionFunc { get; init; } = _ => true; 14 | 15 | public Func ResultFactory { get; init; } = _ => Results.NotFound(); 16 | 17 | public virtual async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) 18 | { 19 | var condition = ConditionFunc.Invoke(context.HttpContext); 20 | if (condition) 21 | { 22 | await next(); 23 | } 24 | else 25 | { 26 | var result = ResultFactory.Invoke(context.HttpContext); 27 | context.Result = result switch 28 | { 29 | IActionResult actionResult => actionResult, 30 | IResult httpResult => new HttpResultActionResultAdapter(httpResult), 31 | _ => new OkObjectResult(result) 32 | }; 33 | } 34 | } 35 | 36 | public virtual async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) 37 | { 38 | var result = ConditionFunc.Invoke(context.HttpContext); 39 | if (result) 40 | { 41 | return await next(context); 42 | } 43 | return ResultFactory.Invoke(context.HttpContext); 44 | } 45 | } 46 | 47 | internal sealed class HttpResultActionResultAdapter(IResult result) : IActionResult 48 | { 49 | public Task ExecuteResultAsync(ActionContext context) 50 | { 51 | return result.ExecuteAsync(context.HttpContext); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Filters/EnvironmentFilter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.Extensions.Hosting; 5 | using WeihanLi.Common; 6 | 7 | namespace WeihanLi.Web.Filters; 8 | 9 | /// 10 | /// Environment filter with allowed environment name 11 | /// 12 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)] 13 | public sealed class EnvironmentFilter : ConditionalFilter 14 | { 15 | public EnvironmentFilter(params string[] environmentNames) 16 | { 17 | Guard.NotNull(environmentNames); 18 | var allowedEnvironments = environmentNames.ToHashSet(StringComparer.OrdinalIgnoreCase); 19 | ConditionFunc = c => 20 | { 21 | var env = c.RequestServices.GetRequiredService().EnvironmentName; 22 | return allowedEnvironments.Contains(env); 23 | }; 24 | } 25 | } 26 | 27 | /// 28 | /// Should work only for non-production 29 | /// 30 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)] 31 | public sealed class NonProductionEnvironmentFilter : ConditionalFilter 32 | { 33 | public NonProductionEnvironmentFilter() 34 | { 35 | ConditionFunc = c => c.RequestServices.GetRequiredService() 36 | .IsProduction() == false; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Filters/FeatureFlagFilter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.Filters; 6 | 7 | namespace WeihanLi.Web.Filters; 8 | 9 | public interface IFeatureFlagFilterResponseFactory 10 | { 11 | public Task GetResponse(ResourceExecutingContext resourceExecutingContext); 12 | } 13 | 14 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] 15 | public sealed class FeatureFlagFilterAttribute(string featureFlagName) : Attribute, IAsyncResourceFilter 16 | { 17 | public bool DefaultValue { get; set; } 18 | public string FeatureFlagName { get; } = featureFlagName ?? throw new ArgumentNullException(nameof(featureFlagName)); 19 | 20 | public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) 21 | { 22 | var configuration = context.HttpContext.RequestServices.GetRequiredService(); 23 | if (configuration.IsFeatureEnabled(FeatureFlagName, DefaultValue)) 24 | { 25 | await next(); 26 | } 27 | else 28 | { 29 | var responseFactory = context.HttpContext.RequestServices 30 | .GetService(); 31 | if (responseFactory != null) 32 | { 33 | context.Result = await responseFactory.GetResponse(context); 34 | } 35 | else 36 | { 37 | context.Result = new NotFoundResult(); 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Formatters/PlainTextInputFormatter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Mvc.Formatters; 5 | using Microsoft.Net.Http.Headers; 6 | using System.Text; 7 | 8 | namespace WeihanLi.Web.Formatters; 9 | 10 | public sealed class PlainTextInputFormatter : TextInputFormatter 11 | { 12 | public PlainTextInputFormatter() 13 | { 14 | SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/plain")); 15 | 16 | SupportedEncodings.Add(Encoding.UTF8); 17 | SupportedEncodings.Add(Encoding.Unicode); 18 | } 19 | 20 | public override async Task ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding) 21 | { 22 | using var reader = context.ReaderFactory(context.HttpContext.Request.Body, encoding); 23 | var rawContent = await reader.ReadToEndAsync(); 24 | return await InputFormatterResult.SuccessAsync(rawContent); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Middleware/ConfigInspectorMiddleware.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | namespace WeihanLi.Web.Middleware; 5 | 6 | public sealed class ConfigInspectorOptions 7 | { 8 | public bool IncludeEmptyProviders { get; set; } 9 | public Func? ConfigRenderer { get; set; } 10 | } 11 | 12 | public sealed class ConfigModel 13 | { 14 | public required string Provider { get; set; } 15 | public ConfigItemModel[] Items { get; set; } = []; 16 | } 17 | 18 | public sealed class ConfigItemModel 19 | { 20 | public required string Key { get; set; } 21 | public string? Value { get; set; } 22 | public bool Active { get; set; } 23 | } 24 | 25 | internal sealed class ConfigInspectorMiddleware 26 | { 27 | public static Task InvokeAsync(HttpContext httpContext, IOptions inspectorOptions) 28 | { 29 | var configuration = httpContext.RequestServices.GetRequiredService(); 30 | if (configuration is not IConfigurationRoot configurationRoot) 31 | { 32 | throw new NotSupportedException( 33 | "Support ConfigurationRoot configuration only, please use the default configuration or implement IConfigurationRoot"); 34 | } 35 | 36 | var inspectorOptionsValue = inspectorOptions.Value; 37 | 38 | var configKey = string.Empty; 39 | if (httpContext.Request.RouteValues.TryGetValue("configKey", out var configKeyObj) && 40 | configKeyObj is string { Length: > 0 } configKeyName) 41 | { 42 | configKey = configKeyName; 43 | } 44 | 45 | var configs = GetConfig(configurationRoot, inspectorOptionsValue, configKey); 46 | if (inspectorOptionsValue.ConfigRenderer is null) 47 | return httpContext.Response.WriteAsJsonAsync(configs); 48 | 49 | return inspectorOptionsValue.ConfigRenderer.Invoke(httpContext, configs); 50 | } 51 | 52 | private static ConfigModel[] GetConfig(IConfigurationRoot configurationRoot, ConfigInspectorOptions options, 53 | string configKey) 54 | { 55 | var allKeys = configurationRoot.AsEnumerable() 56 | .ToDictionary(x => x.Key, _ => false); 57 | 58 | var hasConfigKeyFilter = !string.IsNullOrEmpty(configKey); 59 | if (hasConfigKeyFilter) 60 | { 61 | if (allKeys.TryGetValue(configKey, out _)) 62 | { 63 | allKeys = new() 64 | { 65 | { configKey, false } 66 | }; 67 | } 68 | else 69 | { 70 | return []; 71 | } 72 | } 73 | 74 | var providers = GetConfigProviders(configurationRoot); 75 | var config = new ConfigModel[providers.Count]; 76 | 77 | for (var i = providers.Count - 1; i >= 0; i--) 78 | { 79 | var provider = providers[i]; 80 | config[i] = new ConfigModel 81 | { 82 | Provider = provider.ToString() ?? provider.GetType().Name, 83 | Items = GetConfig(provider, allKeys).ToArray() 84 | }; 85 | } 86 | 87 | if (options.IncludeEmptyProviders) 88 | { 89 | return config; 90 | } 91 | 92 | return config.Where(x => x.Items is { Length: > 0 }).ToArray(); 93 | } 94 | 95 | private static List GetConfigProviders(IConfigurationRoot configurationRoot) 96 | { 97 | var providers = new List(); 98 | 99 | foreach (var provider in configurationRoot.Providers) 100 | { 101 | 102 | if (provider is not ChainedConfigurationProvider chainedConfigurationProvider) 103 | { 104 | providers.Add(provider); 105 | continue; 106 | } 107 | 108 | if (chainedConfigurationProvider.Configuration is not IConfigurationRoot chainsConfigurationRoot) 109 | { 110 | continue; 111 | } 112 | 113 | providers.AddRange(GetConfigProviders(chainsConfigurationRoot)); 114 | } 115 | 116 | return providers; 117 | } 118 | 119 | private static IEnumerable GetConfig(IConfigurationProvider provider, 120 | Dictionary keys) 121 | { 122 | foreach (var (key, active) in keys) 123 | { 124 | if (!provider.TryGet(key, out var value)) 125 | continue; 126 | 127 | var configItem = new ConfigItemModel { Key = key, Value = value }; 128 | if (!active) 129 | { 130 | configItem.Active = keys[key] = true; 131 | } 132 | 133 | yield return configItem; 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Middleware/CustomExceptionHandlerMiddleware.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace WeihanLi.Web.Middleware; 7 | 8 | public sealed class CustomExceptionHandlerOptions 9 | { 10 | public Func? OnException { get; set; } = 11 | (context, logger, exception) => 12 | { 13 | logger.LogError(exception, $"Request exception, requestId: {context.TraceIdentifier}"); 14 | return Task.CompletedTask; 15 | }; 16 | 17 | public Func? OnRequestAborted { get; set; } 18 | } 19 | 20 | public sealed class CustomExceptionHandlerMiddleware( 21 | RequestDelegate next, 22 | IOptions options) 23 | { 24 | private readonly CustomExceptionHandlerOptions _options = options.Value; 25 | 26 | public async Task InvokeAsync(HttpContext context) 27 | { 28 | try 29 | { 30 | await next(context); 31 | } 32 | catch (Exception ex) 33 | { 34 | var logger = context.RequestServices.GetRequiredService() 35 | .CreateLogger(); 36 | if (context.RequestAborted.IsCancellationRequested && ex is OperationCanceledException) 37 | { 38 | _options.OnRequestAborted?.Invoke(context, logger); 39 | } 40 | else 41 | { 42 | _options.OnException?.Invoke(context, logger, ex); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Pager/IPagedListModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | namespace WeihanLi.Web.Pager; 5 | 6 | /// 7 | /// IPagedListModel 8 | /// 9 | /// Type 10 | public interface IPagedListModel : IReadOnlyList 11 | { 12 | /// 13 | /// Data 14 | /// 15 | IReadOnlyList Data { get; } 16 | 17 | /// 18 | /// PagerModel 19 | /// 20 | IPagerModel Pager { get; } 21 | } 22 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Pager/IPagerModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | namespace WeihanLi.Web.Pager; 5 | 6 | /// 7 | /// 分页信息模型接口 8 | /// IPagerModel 9 | /// 10 | public interface IPagerModel 11 | { 12 | /// 13 | /// 分页显示模式 14 | /// 15 | PagingDisplayMode PagingDisplayMode { get; set; } 16 | 17 | /// 18 | /// 页码索引 19 | /// 20 | int PageNumber { get; set; } 21 | 22 | /// 23 | /// 页码容量,每页数据量 24 | /// 25 | int PageSize { get; set; } 26 | 27 | /// 28 | /// 数据总页数 29 | /// 30 | int PageCount { get; set; } 31 | 32 | /// 33 | /// 数据总数量 34 | /// 35 | int TotalCount { get; set; } 36 | 37 | /// 38 | /// 本页第一个元素索引 39 | /// 40 | int FirstItem { get; } 41 | 42 | /// 43 | /// 本页最后一个元素索引 44 | /// 45 | int LastItem { get; } 46 | 47 | /// 48 | /// 是否是第一页 49 | /// 50 | bool IsFirstPage { get; } 51 | 52 | /// 53 | /// 是否是最后一页 54 | /// 55 | bool IsLastPage { get; } 56 | 57 | /// 58 | /// 是否有上一页 59 | /// 60 | bool HasPreviousPage { get; } 61 | 62 | /// 63 | /// 是否有下一页 64 | /// 65 | bool HasNextPage { get; } 66 | 67 | /// 68 | /// 每组数据量 69 | /// 70 | int GroupSize { get; set; } 71 | 72 | /// 73 | /// 是否显示跳转按钮 74 | /// 75 | bool ShowJumpButton { get; set; } 76 | 77 | /// 78 | /// 翻页路径或翻页处理事件 79 | /// 80 | Func? OnPageChange { get; set; } 81 | } 82 | 83 | /// 84 | /// PagingDisplayMode 85 | /// 86 | public enum PagingDisplayMode 87 | { 88 | /// 89 | /// always show the pager 90 | /// 91 | Always = 0, 92 | 93 | /// 94 | /// show pager only when pageSize > 1 95 | /// 96 | IfNeeded = 1 97 | } 98 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Pager/PagedListModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using System.Collections; 5 | 6 | namespace WeihanLi.Web.Pager; 7 | 8 | /// 9 | /// PagedListModel 10 | /// 11 | /// Type 12 | internal sealed class PagedListModel : IPagedListModel 13 | { 14 | public IReadOnlyList Data { get; } 15 | 16 | public IPagerModel Pager { get; } 17 | 18 | public int Count => Data.Count; 19 | 20 | #nullable disable 21 | public PagedListModel(IEnumerable data, IPagerModel pager) 22 | { 23 | Data = data?.ToArray() ?? []; 24 | Pager = pager; 25 | } 26 | #nullable restore 27 | 28 | public IEnumerator GetEnumerator() 29 | { 30 | return Data.GetEnumerator(); 31 | } 32 | 33 | IEnumerator IEnumerable.GetEnumerator() 34 | { 35 | return Data.GetEnumerator(); 36 | } 37 | 38 | public T this[int i] 39 | { 40 | get 41 | { 42 | if (i < 0 || i >= Data.Count) 43 | { 44 | throw new IndexOutOfRangeException(); 45 | } 46 | return Data[i]; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Pager/PagedListModelExtension.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | namespace WeihanLi.Web.Pager; 5 | 6 | public static class PagedListModelExtension 7 | { 8 | public static IPagedListModel ToPagedList(this IEnumerable data, int pageNumber, int pageSize, int totalCount) 9 | { 10 | return new PagedListModel(data, new PagerModel(pageNumber, pageSize, totalCount)); 11 | } 12 | 13 | public static IPagedListModel ToPagedList(this Common.Models.IPagedListResult data) 14 | { 15 | return new PagedListModel(data.Data, new PagerModel(data.PageNumber, data.PageSize, data.TotalCount)); 16 | } 17 | 18 | public static IPagedListModel ToPagedList(this Common.Models.IListResultWithTotal data, int pageNum, int pageSize) 19 | { 20 | return new PagedListModel(data.Data, new PagerModel(pageNum, pageSize, data.TotalCount)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Pager/PagerHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Html; 5 | using Microsoft.AspNetCore.Mvc.Rendering; 6 | 7 | namespace WeihanLi.Web.Pager; 8 | 9 | /// 10 | /// PagerHelper 分页帮助类 11 | /// 12 | public static class PagerHelper 13 | { 14 | /// 15 | /// HtmlHelper Pager - 扩展方法 16 | /// 17 | /// HtmlHelper 18 | /// 分页信息 19 | /// 翻页地址或事件 20 | /// 21 | public static IHtmlContent Pager(this IHtmlHelper helper, IPagerModel pagerModel, Func onPageChange) 22 | { 23 | return helper.Pager(pagerModel, onPageChange, "_PagerPartial"); 24 | } 25 | 26 | /// 27 | /// HtmlHelper Pager - 扩展方法 28 | /// 29 | /// HtmlHelper 30 | /// 分页信息 31 | /// 翻页地址或事件 32 | /// 分页分部视图名称,默认值为【_PagerPartial】 33 | /// 34 | public static IHtmlContent Pager(this IHtmlHelper helper, IPagerModel pagerModel, Func onPageChange, string pagerViewName) 35 | { 36 | return helper.Pager(pagerModel, onPageChange, pagerViewName, PagingDisplayMode.Always); 37 | } 38 | 39 | /// 40 | /// HtmlHelper Pager - 扩展方法 41 | /// 42 | /// HtmlHelper 43 | /// 分页信息 44 | /// 翻页地址或事件 45 | /// 分页分部视图名称,默认值为【_PagerPartial】 46 | /// 分页显示模式 47 | /// 48 | public static IHtmlContent Pager(this IHtmlHelper helper, IPagerModel pagerModel, Func onPageChange, string pagerViewName, PagingDisplayMode displayMode) 49 | { 50 | pagerModel.OnPageChange = onPageChange; 51 | pagerModel.PagingDisplayMode = displayMode; 52 | return helper.Partial(pagerViewName, pagerModel); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Pager/PagerModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | namespace WeihanLi.Web.Pager; 5 | 6 | internal sealed class PagerModel : IPagerModel 7 | { 8 | public PagingDisplayMode PagingDisplayMode { get; set; } 9 | 10 | public int PageNumber { get; set; } 11 | 12 | public int PageSize { get; set; } 13 | 14 | public int PageCount { get; set; } 15 | 16 | public int TotalCount { get; set; } 17 | 18 | public PagerModel(int pageNumber, int pageSize, int totalCount) 19 | { 20 | PageNumber = pageNumber; 21 | PageSize = pageSize; 22 | TotalCount = totalCount; 23 | PageCount = Convert.ToInt32(Math.Ceiling(TotalCount * 1.0 / PageSize)); 24 | } 25 | 26 | public bool IsFirstPage => PageNumber <= 1; 27 | 28 | public bool IsLastPage => PageNumber >= PageCount; 29 | 30 | public bool HasPreviousPage => PageNumber > 1; 31 | 32 | public bool HasNextPage => PageNumber < PageCount; 33 | 34 | public int FirstItem => (PageNumber - 1) * PageSize + 1; 35 | 36 | public int LastItem 37 | { 38 | get 39 | { 40 | if (IsLastPage) 41 | { 42 | return FirstItem + (TotalCount - 1) % PageSize; 43 | } 44 | else 45 | { 46 | return PageNumber * PageSize; 47 | } 48 | } 49 | } 50 | 51 | public Func? OnPageChange { get; set; } 52 | 53 | private int _groupSize = 12; 54 | 55 | public int GroupSize 56 | { 57 | get => _groupSize; 58 | set 59 | { 60 | if (value > 1) 61 | { 62 | _groupSize = value; 63 | } 64 | } 65 | } 66 | 67 | public bool ShowJumpButton { get; set; } 68 | } 69 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Services/HttpContextCancellationTokenProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using WeihanLi.Common.Services; 5 | 6 | namespace WeihanLi.Web.Services; 7 | 8 | public class HttpContextCancellationTokenProvider(IHttpContextAccessor httpContextAccessor) : ICancellationTokenProvider 9 | { 10 | public virtual CancellationToken GetCancellationToken() 11 | { 12 | return httpContextAccessor.HttpContext?.RequestAborted ?? CancellationToken.None; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Services/HttpContextLoggingEnricher.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using WeihanLi.Common.Logging; 5 | using WeihanLi.Web.Extensions; 6 | 7 | namespace WeihanLi.Web.Services; 8 | 9 | public class HttpContextLoggingEnricher : ILogHelperLoggingEnricher 10 | { 11 | private readonly IHttpContextAccessor _contextAccessor; 12 | private readonly Action _enrichAction; 13 | 14 | public HttpContextLoggingEnricher(IHttpContextAccessor contextAccessor) : this(contextAccessor, null) 15 | { 16 | } 17 | 18 | public HttpContextLoggingEnricher(IHttpContextAccessor contextAccessor, Action? enrichAction) 19 | { 20 | _contextAccessor = contextAccessor; 21 | if (enrichAction == null) 22 | { 23 | _enrichAction = (logEvent, httpContext) => 24 | { 25 | logEvent.AddProperty("RequestIP", httpContext.GetUserIP() ?? string.Empty); 26 | logEvent.AddProperty("RequestPath", httpContext.Request.Path); 27 | logEvent.AddProperty("RequestMethod", httpContext.Request.Method); 28 | 29 | logEvent.AddProperty("TraceId", httpContext.Request.Headers["TraceId"].ToString()); 30 | logEvent.AddProperty("Referer", httpContext.Request.Headers["Referer"].ToString()); 31 | }; 32 | } 33 | else 34 | { 35 | _enrichAction = enrichAction; 36 | } 37 | } 38 | 39 | public virtual void Enrich(LogHelperLoggingEvent loggingEvent) 40 | { 41 | if (_contextAccessor.HttpContext is not null) 42 | { 43 | _enrichAction.Invoke(loggingEvent, _contextAccessor.HttpContext); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Services/HttpContextTenantProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using WeihanLi.Common.Models; 5 | using WeihanLi.Common.Services; 6 | 7 | namespace WeihanLi.Web.Services; 8 | 9 | public sealed class HttpContextTenantProviderOptions 10 | { 11 | private Func _tenantIdFactory = context => context.User.FindFirst("tenantId")?.Value; 12 | 13 | private Func _tenantInfoFactory = context => 14 | { 15 | var tenantId = context.User.FindFirst("tenantId")?.Value; 16 | if (string.IsNullOrEmpty(tenantId)) 17 | { 18 | return null; 19 | } 20 | return new TenantInfo() 21 | { 22 | TenantId = tenantId, 23 | TenantName = context.User.FindFirst("tenantName")?.Value 24 | }; 25 | }; 26 | 27 | public Func TenantIdFactory 28 | { 29 | get => _tenantIdFactory; 30 | set 31 | { 32 | ArgumentNullException.ThrowIfNull(value, nameof(TenantIdFactory)); 33 | _tenantIdFactory = value; 34 | } 35 | } 36 | 37 | public Func TenantInfoFactory 38 | { 39 | get => _tenantInfoFactory; 40 | set 41 | { 42 | ArgumentNullException.ThrowIfNull(value, nameof(TenantInfoFactory)); 43 | _tenantInfoFactory = value; 44 | } 45 | } 46 | } 47 | 48 | public sealed class HttpContextTenantProvider : ITenantProvider 49 | { 50 | private readonly IHttpContextAccessor _httpContextAccessor; 51 | private readonly IOptions _options; 52 | 53 | public HttpContextTenantProvider( 54 | IHttpContextAccessor httpContextAccessor, 55 | IOptions options 56 | ) 57 | { 58 | _httpContextAccessor = httpContextAccessor; 59 | _options = options; 60 | } 61 | 62 | public string? GetTenantId() 63 | { 64 | if (_httpContextAccessor.HttpContext is null) 65 | { 66 | return null; 67 | } 68 | 69 | return _options.Value.TenantIdFactory.Invoke(_httpContextAccessor.HttpContext); 70 | } 71 | 72 | public TenantInfo? GetTenantInfo() 73 | { 74 | if (_httpContextAccessor.HttpContext is null) 75 | { 76 | return null; 77 | } 78 | 79 | return _options.Value.TenantInfoFactory.Invoke(_httpContextAccessor.HttpContext); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/Services/HttpContextUserIdProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Weihan Li. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using WeihanLi.Common.Services; 5 | using WeihanLi.Web.Extensions; 6 | 7 | namespace WeihanLi.Web.Services; 8 | 9 | public sealed class HttpContextUserIdProviderOptions 10 | { 11 | private Func _userIdFactory = context => context.User.GetUserId(); 12 | 13 | public Func UserIdFactory 14 | { 15 | get => _userIdFactory; 16 | set 17 | { 18 | ArgumentNullException.ThrowIfNull(value, nameof(UserIdFactory)); 19 | _userIdFactory = value; 20 | } 21 | } 22 | } 23 | 24 | public sealed class HttpContextUserIdProvider : IUserIdProvider 25 | { 26 | private readonly IHttpContextAccessor _httpContextAccessor; 27 | private readonly Func _userIdFactory; 28 | 29 | public HttpContextUserIdProvider( 30 | IHttpContextAccessor httpContextAccessor, 31 | IOptions options 32 | ) 33 | { 34 | _httpContextAccessor = httpContextAccessor; 35 | _userIdFactory = options.Value.UserIdFactory; 36 | } 37 | 38 | public string? GetUserId() 39 | { 40 | if (_httpContextAccessor.HttpContext is null) 41 | { 42 | return null; 43 | } 44 | 45 | return _userIdFactory.Invoke(_httpContextAccessor.HttpContext); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/WeihanLi.Web.Extensions/WeihanLi.Web.Extensions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | WeihanLi.Web.Extensions 4 | WeihanLi.Web.Extensions 5 | 6 | net8.0;net9.0;net10.0 7 | WeihanLi.Web.Extensions 8 | weihanli;web;aspnet;aspnetcore;mvc;authentication;authorization;auth;token 9 | WeihanLi.Web.Extensions, aspnetcore extensions 10 | https://github.com/WeihanLi/WeihanLi.Web.Extensions/tree/dev/src/WeihanLi.Web.Extensions 11 | WeihanLi.Web 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /toc.yml: -------------------------------------------------------------------------------- 1 | - name: Home 2 | href: README.md 3 | - name: API Documentation 4 | href: docs/api 5 | 6 | --------------------------------------------------------------------------------