├── .editorconfig ├── .gitattributes ├── .github ├── actions │ └── dotnet │ │ └── action.yml ├── dependabot.yml ├── release.yml └── workflows │ ├── build.yml │ ├── changelog.config │ ├── changelog.yml │ ├── dotnet-file.yml │ ├── includes.yml │ ├── publish.yml │ └── triage.yml ├── .gitignore ├── .netconfig ├── Directory.Build.rsp ├── WebSocketChannel.sln ├── _config.yml ├── assets ├── css │ └── style.scss └── img │ ├── icon.png │ ├── icon.svg │ ├── noun_TV_2679303.svg │ └── noun_web_849841.svg ├── changelog.md ├── global.json ├── license.txt ├── readme.md └── src ├── Benchmark ├── Benchmark.csproj └── Program.cs ├── CodeAnalysis ├── CodeAnalysis.csproj └── WebSocketChannel.targets ├── Directory.Build.props ├── Directory.Build.targets ├── Directory.props ├── SponsorLink ├── Analyzer │ ├── Analyzer.csproj │ ├── GraceApiAnalyzer.cs │ ├── Properties │ │ └── launchSettings.json │ ├── StatusReportingAnalyzer.cs │ ├── StatusReportingGenerator.cs │ └── buildTransitive │ │ └── SponsorableLib.targets ├── Directory.Build.props ├── Directory.Build.targets ├── Library │ ├── Library.csproj │ ├── MyClass.cs │ ├── Resources.resx │ └── readme.md ├── SponsorLink.Analyzer.Tests.targets ├── SponsorLink.Analyzer.targets ├── SponsorLink │ ├── AnalyzerOptionsExtensions.cs │ ├── AppDomainDictionary.cs │ ├── DiagnosticsManager.cs │ ├── Resources.es-AR.resx │ ├── Resources.es.resx │ ├── Resources.resx │ ├── SponsorLink.cs │ ├── SponsorLink.csproj │ ├── SponsorLinkAnalyzer.cs │ ├── SponsorManifest.cs │ ├── SponsorStatus.cs │ ├── SponsorableLib.targets │ ├── Tracing.cs │ ├── buildTransitive │ │ └── Devlooped.Sponsors.targets │ └── sponsorable.md ├── SponsorLinkAnalyzer.sln ├── Tests │ ├── .netconfig │ ├── AnalyzerTests.cs │ ├── Attributes.cs │ ├── Extensions.cs │ ├── JsonOptions.cs │ ├── Resources.resx │ ├── Sample.cs │ ├── SponsorManifestTests.cs │ ├── SponsorableManifest.cs │ ├── Tests.csproj │ └── keys │ │ ├── kzu.key │ │ ├── kzu.key.jwk │ │ ├── kzu.key.txt │ │ ├── kzu.pub │ │ ├── kzu.pub.jwk │ │ ├── kzu.pub.txt │ │ └── sponsorlink.jwt ├── jwk.ps1 └── readme.md ├── Tests ├── Properties │ └── launchSettings.json ├── Tests.csproj ├── WebSocketChannelTests.cs ├── WebSocketServer.cs └── xunit.runner.json ├── WebSocketChannel ├── Visibility.cs ├── WebSocketChannel.cs ├── WebSocketChannel.csproj ├── WebSocketExtensions.cs └── readme.md ├── icon.png ├── kzu.snk └── nuget.config /.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 | 15 | # Xml project files 16 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,msbuildproj,props,targets}] 17 | indent_size = 2 18 | 19 | # Xml config files 20 | [*.{ruleset,config,nuspec,resx,vsixmanifest,vsct}] 21 | indent_size = 2 22 | 23 | # YAML files 24 | [*.{yaml,yml}] 25 | indent_size = 2 26 | 27 | # JSON files 28 | [*.json] 29 | indent_size = 2 30 | 31 | # Dotnet code style settings: 32 | [*.{cs,vb}] 33 | # Sort using and Import directives with System.* appearing first 34 | dotnet_sort_system_directives_first = true 35 | # Avoid "this." and "Me." if not necessary 36 | dotnet_style_qualification_for_field = false:suggestion 37 | dotnet_style_qualification_for_property = false:suggestion 38 | dotnet_style_qualification_for_method = false:suggestion 39 | dotnet_style_qualification_for_event = false:suggestion 40 | 41 | # Use language keywords instead of framework type names for type references 42 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 43 | dotnet_style_predefined_type_for_member_access = true:suggestion 44 | 45 | # Suggest more modern language features when available 46 | dotnet_style_object_initializer = true:suggestion 47 | dotnet_style_collection_initializer = true:suggestion 48 | dotnet_style_coalesce_expression = true:suggestion 49 | dotnet_style_null_propagation = true:suggestion 50 | dotnet_style_explicit_tuple_names = true:suggestion 51 | 52 | # CSharp code style settings: 53 | 54 | # IDE0040: Add accessibility modifiers 55 | dotnet_style_require_accessibility_modifiers = omit_if_default:error 56 | 57 | # IDE0040: Add accessibility modifiers 58 | dotnet_diagnostic.IDE0040.severity = error 59 | 60 | [*.cs] 61 | # Top-level files are definitely OK 62 | csharp_using_directive_placement = outside_namespace:silent 63 | csharp_style_namespace_declarations = block_scoped:silent 64 | csharp_prefer_simple_using_statement = true:suggestion 65 | csharp_prefer_braces = true:silent 66 | 67 | # Prefer "var" everywhere 68 | csharp_style_var_for_built_in_types = true:suggestion 69 | csharp_style_var_when_type_is_apparent = true:suggestion 70 | csharp_style_var_elsewhere = true:suggestion 71 | 72 | # Prefer method-like constructs to have an expression-body 73 | csharp_style_expression_bodied_methods = true:none 74 | csharp_style_expression_bodied_constructors = true:none 75 | csharp_style_expression_bodied_operators = true:none 76 | 77 | # Prefer property-like constructs to have an expression-body 78 | csharp_style_expression_bodied_properties = true:none 79 | csharp_style_expression_bodied_indexers = true:none 80 | csharp_style_expression_bodied_accessors = true:none 81 | 82 | # Suggest more modern language features when available 83 | csharp_style_pattern_matching_over_is_with_cast_check = true:error 84 | csharp_style_pattern_matching_over_as_with_null_check = true:error 85 | csharp_style_inlined_variable_declaration = true:suggestion 86 | csharp_style_throw_expression = true:suggestion 87 | csharp_style_conditional_delegate_call = true:suggestion 88 | 89 | # Newline settings 90 | csharp_new_line_before_open_brace = all 91 | csharp_new_line_before_else = true 92 | csharp_new_line_before_catch = true 93 | csharp_new_line_before_finally = true 94 | csharp_new_line_before_members_in_object_initializers = true 95 | csharp_new_line_before_members_in_anonymous_types = true 96 | 97 | # Test settings 98 | [**/*Tests*/**{.cs,.vb}] 99 | # xUnit1013: Public method should be marked as test. Allows using records as test classes 100 | dotnet_diagnostic.xUnit1013.severity = none 101 | 102 | # CS9113: Parameter is unread (usually, ITestOutputHelper) 103 | dotnet_diagnostic.CS9113.severity = none 104 | 105 | # Default severity for analyzer diagnostics with category 'Style' 106 | dotnet_analyzer_diagnostic.category-Style.severity = none 107 | 108 | # VSTHRD200: Use "Async" suffix for async methods 109 | dotnet_diagnostic.VSTHRD200.severity = none 110 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # normalize by default 2 | * text=auto encoding=UTF-8 3 | *.sh text eol=lf 4 | 5 | # These are windows specific files which we may as well ensure are 6 | # always crlf on checkout 7 | *.bat text eol=crlf 8 | *.cmd text eol=crlf 9 | -------------------------------------------------------------------------------- /.github/actions/dotnet/action.yml: -------------------------------------------------------------------------------- 1 | name: ⚙ dotnet 2 | description: Configures dotnet if the repo/org defines the DOTNET custom property 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - name: 🔎 dotnet 8 | id: dotnet 9 | shell: bash 10 | run: | 11 | VERSIONS=$(gh api repos/${{ github.repository }}/properties/values | jq -r '.[] | select(.property_name == "DOTNET") | .value') 12 | # Remove extra whitespace from VERSIONS 13 | VERSIONS=$(echo "$VERSIONS" | tr -s ' ' | tr -d ' ') 14 | # Convert comma-separated to newline-separated 15 | NEWLINE_VERSIONS=$(echo "$VERSIONS" | tr ',' '\n') 16 | # Validate versions 17 | while IFS= read -r version; do 18 | if ! [[ $version =~ ^[0-9]+(\.[0-9]+(\.[0-9]+)?)?(\.x)?$ ]]; then 19 | echo "Error: Invalid version format: $version" 20 | exit 1 21 | fi 22 | done <<< "$NEWLINE_VERSIONS" 23 | # Write multiline output to $GITHUB_OUTPUT 24 | { 25 | echo 'versions<> $GITHUB_OUTPUT 29 | 30 | - name: ⚙ dotnet 31 | if: steps.dotnet.outputs.versions != '' 32 | uses: actions/setup-dotnet@v4 33 | with: 34 | dotnet-version: | 35 | ${{ steps.dotnet.outputs.versions }} 36 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: nuget 7 | directory: / 8 | schedule: 9 | interval: daily 10 | groups: 11 | Azure: 12 | patterns: 13 | - "Azure*" 14 | - "Microsoft.Azure*" 15 | Identity: 16 | patterns: 17 | - "System.IdentityModel*" 18 | - "Microsoft.IdentityModel*" 19 | System: 20 | patterns: 21 | - "System*" 22 | exclude-patterns: 23 | - "System.IdentityModel*" 24 | Extensions: 25 | patterns: 26 | - "Microsoft.Extensions*" 27 | Web: 28 | patterns: 29 | - "Microsoft.AspNetCore*" 30 | Tests: 31 | patterns: 32 | - "Microsoft.NET.Test*" 33 | - "xunit*" 34 | - "coverlet*" 35 | ThisAssembly: 36 | patterns: 37 | - "ThisAssembly*" 38 | ProtoBuf: 39 | patterns: 40 | - "protobuf-*" 41 | Spectre: 42 | patterns: 43 | - "Spectre.Console*" 44 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - bydesign 5 | - dependencies 6 | - duplicate 7 | - question 8 | - invalid 9 | - wontfix 10 | - need info 11 | - techdebt 12 | authors: 13 | - devlooped-bot 14 | - dependabot 15 | - github-actions 16 | categories: 17 | - title: ✨ Implemented enhancements 18 | labels: 19 | - enhancement 20 | - title: 🐛 Fixed bugs 21 | labels: 22 | - bug 23 | - title: 📝 Documentation updates 24 | labels: 25 | - docs 26 | - documentation 27 | - title: 🔨 Other 28 | labels: 29 | - '*' 30 | exclude: 31 | labels: 32 | - dependencies 33 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # Builds and runs tests in all three supported OSes 2 | # Pushes CI feed if secrets.SLEET_CONNECTION is provided 3 | 4 | name: build 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | configuration: 9 | type: choice 10 | description: Configuration 11 | options: 12 | - Release 13 | - Debug 14 | push: 15 | branches: [ main, dev, 'dev/*', 'feature/*', 'rel/*' ] 16 | paths-ignore: 17 | - changelog.md 18 | - readme.md 19 | pull_request: 20 | types: [opened, synchronize, reopened] 21 | 22 | env: 23 | DOTNET_NOLOGO: true 24 | PackOnBuild: true 25 | GeneratePackageOnBuild: true 26 | VersionPrefix: 42.42.${{ github.run_number }} 27 | VersionLabel: ${{ github.ref }} 28 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 29 | MSBUILDTERMINALLOGGER: auto 30 | Configuration: ${{ github.event.inputs.configuration || 'Release' }} 31 | SLEET_FEED_URL: ${{ vars.SLEET_FEED_URL }} 32 | 33 | defaults: 34 | run: 35 | shell: bash 36 | 37 | jobs: 38 | os-matrix: 39 | runs-on: ubuntu-latest 40 | outputs: 41 | matrix: ${{ steps.lookup.outputs.matrix }} 42 | steps: 43 | - name: 🤘 checkout 44 | uses: actions/checkout@v4 45 | 46 | - name: 🔎 lookup 47 | id: lookup 48 | shell: pwsh 49 | run: | 50 | $path = './.github/workflows/os-matrix.json' 51 | $os = if (test-path $path) { cat $path } else { '["ubuntu-latest"]' } 52 | echo "matrix=$os" >> $env:GITHUB_OUTPUT 53 | 54 | build: 55 | needs: os-matrix 56 | name: build-${{ matrix.os }} 57 | runs-on: ${{ matrix.os }} 58 | strategy: 59 | matrix: 60 | os: ${{ fromJSON(needs.os-matrix.outputs.matrix) }} 61 | steps: 62 | - name: 🤘 checkout 63 | uses: actions/checkout@v4 64 | with: 65 | submodules: recursive 66 | fetch-depth: 0 67 | 68 | - name: ⚙ dotnet 69 | uses: ./.github/actions/dotnet 70 | 71 | - name: 🙏 build 72 | run: dotnet build -m:1 -bl:build.binlog 73 | 74 | - name: 🧪 test 75 | run: | 76 | dotnet tool update -g dotnet-retest 77 | dotnet retest -- --no-build 78 | 79 | - name: 🐛 logs 80 | uses: actions/upload-artifact@v4 81 | if: runner.debug && always() 82 | with: 83 | name: logs 84 | path: '*.binlog' 85 | 86 | - name: 🚀 sleet 87 | env: 88 | SLEET_CONNECTION: ${{ secrets.SLEET_CONNECTION }} 89 | if: env.SLEET_CONNECTION != '' 90 | run: | 91 | dotnet tool update sleet -g --allow-downgrade --version $(curl -s --compressed ${{ vars.SLEET_FEED_URL }} | jq '.["sleet:version"]' -r) 92 | sleet push bin --config none -f --verbose -p "SLEET_FEED_CONTAINER=nuget" -p "SLEET_FEED_CONNECTIONSTRING=${{ secrets.SLEET_CONNECTION }}" -p "SLEET_FEED_TYPE=azure" || echo "No packages found" 93 | 94 | dotnet-format: 95 | runs-on: ubuntu-latest 96 | steps: 97 | - name: 🤘 checkout 98 | uses: actions/checkout@v4 99 | with: 100 | submodules: recursive 101 | fetch-depth: 0 102 | 103 | - name: ⚙ dotnet 104 | uses: actions/setup-dotnet@v4 105 | with: 106 | dotnet-version: | 107 | 6.x 108 | 8.x 109 | 9.x 110 | 111 | - name: ✓ ensure format 112 | run: | 113 | dotnet format whitespace --verify-no-changes -v:diag --exclude ~/.nuget 114 | dotnet format style --verify-no-changes -v:diag --exclude ~/.nuget 115 | -------------------------------------------------------------------------------- /.github/workflows/changelog.config: -------------------------------------------------------------------------------- 1 | usernames-as-github-logins=true 2 | issues_wo_labels=true 3 | pr_wo_labels=true 4 | exclude-labels=bydesign,dependencies,duplicate,discussion,question,invalid,wontfix,need info,docs 5 | enhancement-label=:sparkles: Implemented enhancements: 6 | bugs-label=:bug: Fixed bugs: 7 | issues-label=:hammer: Other: 8 | pr-label=:twisted_rightwards_arrows: Merged: 9 | unreleased=false 10 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: changelog 2 | on: 3 | workflow_dispatch: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | changelog: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: 🤖 defaults 12 | uses: devlooped/actions-bot@v1 13 | with: 14 | name: ${{ secrets.BOT_NAME }} 15 | email: ${{ secrets.BOT_EMAIL }} 16 | gh_token: ${{ secrets.GH_TOKEN }} 17 | github_token: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | - name: 🤘 checkout 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | ref: main 24 | token: ${{ env.GH_TOKEN }} 25 | 26 | - name: ⚙ ruby 27 | uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: 3.0.3 30 | 31 | - name: ⚙ changelog 32 | run: | 33 | gem install github_changelog_generator 34 | github_changelog_generator --user ${GITHUB_REPOSITORY%/*} --project ${GITHUB_REPOSITORY##*/} --token $GH_TOKEN --o changelog.md --config-file .github/workflows/changelog.config 35 | 36 | - name: 🚀 changelog 37 | run: | 38 | git add changelog.md 39 | (git commit -m "🖉 Update changelog with ${GITHUB_REF#refs/*/}" && git push) || echo "Done" -------------------------------------------------------------------------------- /.github/workflows/dotnet-file.yml: -------------------------------------------------------------------------------- 1 | # Synchronizes .netconfig-configured files with dotnet-file 2 | name: dotnet-file 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 0 * * *" 7 | push: 8 | branches: [ 'dotnet-file' ] 9 | 10 | env: 11 | DOTNET_NOLOGO: true 12 | 13 | jobs: 14 | run: 15 | permissions: 16 | contents: write 17 | uses: devlooped/oss/.github/workflows/dotnet-file-core.yml@main 18 | secrets: 19 | BOT_NAME: ${{ secrets.BOT_NAME }} 20 | BOT_EMAIL: ${{ secrets.BOT_EMAIL }} 21 | GH_TOKEN: ${{ secrets.GH_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/includes.yml: -------------------------------------------------------------------------------- 1 | name: +Mᐁ includes 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - 'main' 7 | paths: 8 | - '**.md' 9 | - '!changelog.md' 10 | 11 | jobs: 12 | includes: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | pull-requests: write 17 | steps: 18 | - name: 🤖 defaults 19 | uses: devlooped/actions-bot@v1 20 | with: 21 | name: ${{ secrets.BOT_NAME }} 22 | email: ${{ secrets.BOT_EMAIL }} 23 | gh_token: ${{ secrets.GH_TOKEN }} 24 | github_token: ${{ secrets.GITHUB_TOKEN }} 25 | 26 | - name: 🤘 checkout 27 | uses: actions/checkout@v4 28 | with: 29 | token: ${{ env.GH_TOKEN }} 30 | 31 | - name: +Mᐁ includes 32 | uses: devlooped/actions-includes@v1 33 | 34 | - name: ✍ pull request 35 | uses: peter-evans/create-pull-request@v6 36 | with: 37 | add-paths: '**.md' 38 | base: main 39 | branch: markdown-includes 40 | delete-branch: true 41 | labels: docs 42 | author: ${{ env.BOT_AUTHOR }} 43 | committer: ${{ env.BOT_AUTHOR }} 44 | commit-message: +Mᐁ includes 45 | title: +Mᐁ includes 46 | body: +Mᐁ includes 47 | token: ${{ env.GH_TOKEN }} 48 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # Builds a final release version and pushes to nuget.org 2 | # whenever a release is published. 3 | # Requires: secrets.NUGET_API_KEY 4 | 5 | name: publish 6 | on: 7 | release: 8 | types: [prereleased, released] 9 | 10 | env: 11 | DOTNET_NOLOGO: true 12 | Configuration: Release 13 | PackOnBuild: true 14 | GeneratePackageOnBuild: true 15 | VersionLabel: ${{ github.ref }} 16 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 17 | MSBUILDTERMINALLOGGER: auto 18 | SLEET_FEED_URL: https://api.nuget.org/v3/index.json 19 | 20 | jobs: 21 | publish: 22 | runs-on: ${{ vars.PUBLISH_AGENT || 'ubuntu-latest' }} 23 | steps: 24 | - name: 🤘 checkout 25 | uses: actions/checkout@v4 26 | with: 27 | submodules: recursive 28 | fetch-depth: 0 29 | 30 | - name: ⚙ dotnet 31 | uses: ./.github/actions/dotnet 32 | 33 | - name: 🙏 build 34 | run: dotnet build -m:1 -bl:build.binlog 35 | 36 | - name: 🧪 test 37 | run: | 38 | dotnet tool update -g dotnet-retest 39 | dotnet retest -- --no-build 40 | 41 | - name: 🐛 logs 42 | uses: actions/upload-artifact@v4 43 | if: runner.debug && always() 44 | with: 45 | name: logs 46 | path: '*.binlog' 47 | 48 | - name: 🚀 nuget 49 | env: 50 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} 51 | if: ${{ env.NUGET_API_KEY != '' && github.event.action != 'prereleased' }} 52 | working-directory: bin 53 | run: dotnet nuget push *.nupkg -s https://api.nuget.org/v3/index.json -k ${{secrets.NUGET_API_KEY}} --skip-duplicate 54 | 55 | - name: 🚀 sleet 56 | env: 57 | SLEET_CONNECTION: ${{ secrets.SLEET_CONNECTION }} 58 | if: env.SLEET_CONNECTION != '' 59 | run: | 60 | dotnet tool update sleet -g --allow-downgrade --version $(curl -s --compressed ${{ vars.SLEET_FEED_URL }} | jq '.["sleet:version"]' -r) 61 | sleet push bin --config none -f --verbose -p "SLEET_FEED_CONTAINER=nuget" -p "SLEET_FEED_CONNECTIONSTRING=${{ secrets.SLEET_CONNECTION }}" -p "SLEET_FEED_TYPE=azure" || echo "No packages found" 62 | -------------------------------------------------------------------------------- /.github/workflows/triage.yml: -------------------------------------------------------------------------------- 1 | name: 'triage' 2 | on: 3 | schedule: 4 | - cron: '42 0 * * *' 5 | 6 | workflow_dispatch: 7 | # Manual triggering through the GitHub UI, API, or CLI 8 | inputs: 9 | daysBeforeClose: 10 | description: "Days before closing stale or need info issues" 11 | required: true 12 | default: "30" 13 | daysBeforeStale: 14 | description: "Days before labeling stale" 15 | required: true 16 | default: "180" 17 | daysSinceClose: 18 | description: "Days since close to lock" 19 | required: true 20 | default: "30" 21 | daysSinceUpdate: 22 | description: "Days since update to lock" 23 | required: true 24 | default: "30" 25 | 26 | permissions: 27 | actions: write # For managing the operation state cache 28 | issues: write 29 | contents: read 30 | 31 | jobs: 32 | stale: 33 | # Do not run on forks 34 | if: github.repository_owner == 'devlooped' 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: ⌛ rate 38 | shell: pwsh 39 | if: github.event_name != 'workflow_dispatch' 40 | env: 41 | GH_TOKEN: ${{ secrets.DEVLOOPED_TOKEN }} 42 | run: | 43 | # add random sleep since we run on fixed schedule 44 | $wait = get-random -max 180 45 | echo "Waiting random $wait seconds to start" 46 | sleep $wait 47 | # get currently authenticated user rate limit info 48 | $rate = gh api rate_limit | convertfrom-json | select -expandproperty rate 49 | # if we don't have at least 100 requests left, wait until reset 50 | if ($rate.remaining -lt 100) { 51 | $wait = ($rate.reset - (Get-Date (Get-Date).ToUniversalTime() -UFormat %s)) 52 | echo "Rate limit remaining is $($rate.remaining), waiting for $($wait / 1000) seconds to reset" 53 | sleep $wait 54 | $rate = gh api rate_limit | convertfrom-json | select -expandproperty rate 55 | echo "Rate limit has reset to $($rate.remaining) requests" 56 | } 57 | 58 | - name: ✏️ stale labeler 59 | # pending merge: https://github.com/actions/stale/pull/1176 60 | uses: kzu/stale@c8450312ba97b204bf37545cb249742144d6ca69 61 | with: 62 | ascending: true # Process the oldest issues first 63 | stale-issue-label: 'stale' 64 | stale-issue-message: | 65 | Due to lack of recent activity, this issue has been labeled as 'stale'. 66 | It will be closed if no further activity occurs within ${{ fromJson(inputs.daysBeforeClose || 30 ) }} more days. 67 | Any new comment will remove the label. 68 | close-issue-message: | 69 | This issue will now be closed since it has been labeled 'stale' without activity for ${{ fromJson(inputs.daysBeforeClose || 30 ) }} days. 70 | days-before-stale: ${{ fromJson(inputs.daysBeforeStale || 180) }} 71 | days-before-close: ${{ fromJson(inputs.daysBeforeClose || 30 ) }} 72 | days-before-pr-close: -1 # Do not close PRs labeled as 'stale' 73 | exempt-all-milestones: true 74 | exempt-all-assignees: true 75 | exempt-issue-labels: priority,sponsor,backed 76 | exempt-authors: kzu 77 | 78 | - name: 🤘 checkout actions 79 | uses: actions/checkout@v4 80 | with: 81 | repository: 'microsoft/vscode-github-triage-actions' 82 | ref: v42 83 | 84 | - name: ⚙ install actions 85 | run: npm install --production 86 | 87 | - name: 🔒 issues locker 88 | uses: ./locker 89 | with: 90 | token: ${{ secrets.DEVLOOPED_TOKEN }} 91 | ignoredLabel: priority 92 | daysSinceClose: ${{ fromJson(inputs.daysSinceClose || 30) }} 93 | daysSinceUpdate: ${{ fromJson(inputs.daysSinceUpdate || 30) }} 94 | 95 | - name: 🔒 need info closer 96 | uses: ./needs-more-info-closer 97 | with: 98 | token: ${{ secrets.DEVLOOPED_TOKEN }} 99 | label: 'need info' 100 | closeDays: ${{ fromJson(inputs.daysBeforeClose || 30) }} 101 | closeComment: "This issue has been closed automatically because it needs more information and has not had recent activity.\n\nHappy Coding!" 102 | pingDays: 80 103 | pingComment: "Hey @${assignee}, this issue might need further attention.\n\n@${author}, you can help us out by closing this issue if the problem no longer exists, or adding more information." -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | artifacts 4 | pack 5 | TestResults 6 | results 7 | BenchmarkDotNet.Artifacts 8 | /app 9 | .vs 10 | .vscode 11 | .genaiscript 12 | .idea 13 | local.settings.json 14 | 15 | *.suo 16 | *.sdf 17 | *.userprefs 18 | *.user 19 | *.nupkg 20 | *.metaproj 21 | *.tmp 22 | *.log 23 | *.cache 24 | *.binlog 25 | *.zip 26 | __azurite*.* 27 | __*__ 28 | 29 | .nuget 30 | *.lock.json 31 | *.nuget.props 32 | *.nuget.targets 33 | 34 | node_modules 35 | _site 36 | .jekyll-metadata 37 | .jekyll-cache 38 | .sass-cache 39 | Gemfile.lock 40 | package-lock.json 41 | -------------------------------------------------------------------------------- /Directory.Build.rsp: -------------------------------------------------------------------------------- 1 | # See https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-response-files 2 | -nr:false 3 | -m:1 4 | -v:m 5 | -clp:Summary;ForceNoAlign 6 | -------------------------------------------------------------------------------- /WebSocketChannel.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31710.8 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E45162B8-B80A-4331-A354-7D507B23D97B}" 7 | ProjectSection(SolutionItems) = preProject 8 | .editorconfig = .editorconfig 9 | readme.md = readme.md 10 | EndProjectSection 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebSocketChannel", "src\WebSocketChannel\WebSocketChannel.csproj", "{36D496E4-50C8-4156-8A9F-D525A3C19746}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "src\Tests\Tests.csproj", "{517F1129-4EA6-46FA-827B-42CF5EB0DE09}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmark", "src\Benchmark\Benchmark.csproj", "{694ED796-BC51-4B41-85B0-961E79A424DC}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeAnalysis", "src\CodeAnalysis\CodeAnalysis.csproj", "{E37B743E-69A4-4A24-AD41-9FD3F268A5DF}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Release|Any CPU = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {36D496E4-50C8-4156-8A9F-D525A3C19746}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {36D496E4-50C8-4156-8A9F-D525A3C19746}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {36D496E4-50C8-4156-8A9F-D525A3C19746}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {36D496E4-50C8-4156-8A9F-D525A3C19746}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {517F1129-4EA6-46FA-827B-42CF5EB0DE09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {517F1129-4EA6-46FA-827B-42CF5EB0DE09}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {517F1129-4EA6-46FA-827B-42CF5EB0DE09}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {517F1129-4EA6-46FA-827B-42CF5EB0DE09}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {694ED796-BC51-4B41-85B0-961E79A424DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {694ED796-BC51-4B41-85B0-961E79A424DC}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {694ED796-BC51-4B41-85B0-961E79A424DC}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {694ED796-BC51-4B41-85B0-961E79A424DC}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {E37B743E-69A4-4A24-AD41-9FD3F268A5DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {E37B743E-69A4-4A24-AD41-9FD3F268A5DF}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {E37B743E-69A4-4A24-AD41-9FD3F268A5DF}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {E37B743E-69A4-4A24-AD41-9FD3F268A5DF}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {D4B0A4E9-B519-4C31-939F-0F5BF858248F} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate 2 | 3 | exclude: [ 'src/', '*.sln', 'Gemfile*', '*.rsp' ] -------------------------------------------------------------------------------- /assets/css/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "jekyll-theme-slate"; 5 | 6 | .inner { 7 | max-width: 960px; 8 | } 9 | 10 | pre, code { 11 | background-color: unset; 12 | font-size: unset; 13 | } 14 | 15 | code { 16 | font-size: 0.80em; 17 | } 18 | 19 | h1 > img { 20 | border: unset; 21 | box-shadow: unset; 22 | vertical-align: middle; 23 | -moz-box-shadow: unset; 24 | -o-box-shadow: unset; 25 | -ms-box-shadow: unset; 26 | } 27 | -------------------------------------------------------------------------------- /assets/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devlooped/WebSocketChannel/5b9d995dc1e7503d0e9d56f23cb22e9aa0c5bbfa/assets/img/icon.png -------------------------------------------------------------------------------- /assets/img/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/img/noun_TV_2679303.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/img/noun_web_849841.svg: -------------------------------------------------------------------------------- 1 | 2 | Artboard 24 3 | 4 | 5 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v1.2.0](https://github.com/devlooped/WebSocketChannel/tree/v1.2.0) (2025-02-09) 4 | 5 | [Full Changelog](https://github.com/devlooped/WebSocketChannel/compare/v1.1.0...v1.2.0) 6 | 7 | :sparkles: Implemented enhancements: 8 | 9 | - 💟 Add SponsorLink to ensure ongoing maintenance [\#52](https://github.com/devlooped/WebSocketChannel/pull/52) (@kzu) 10 | 11 | :bug: Fixed bugs: 12 | 13 | - Fix the ID of the funding product and set 30 days grace perior [\#59](https://github.com/devlooped/WebSocketChannel/pull/59) (@kzu) 14 | 15 | :twisted_rightwards_arrows: Merged: 16 | 17 | - Make tests more reliable by checking for a free port [\#60](https://github.com/devlooped/WebSocketChannel/pull/60) (@kzu) 18 | 19 | ## [v1.1.0](https://github.com/devlooped/WebSocketChannel/tree/v1.1.0) (2023-08-11) 20 | 21 | [Full Changelog](https://github.com/devlooped/WebSocketChannel/compare/v1.0.1...v1.1.0) 22 | 23 | :twisted_rightwards_arrows: Merged: 24 | 25 | - Remove current implementation of SponsorLink for now [\#47](https://github.com/devlooped/WebSocketChannel/pull/47) (@kzu) 26 | 27 | ## [v1.0.1](https://github.com/devlooped/WebSocketChannel/tree/v1.0.1) (2023-04-06) 28 | 29 | [Full Changelog](https://github.com/devlooped/WebSocketChannel/compare/v1.0.0...v1.0.1) 30 | 31 | ## [v1.0.0](https://github.com/devlooped/WebSocketChannel/tree/v1.0.0) (2023-04-06) 32 | 33 | [Full Changelog](https://github.com/devlooped/WebSocketChannel/compare/v0.9.2...v1.0.0) 34 | 35 | :sparkles: Implemented enhancements: 36 | 37 | - Add license and autogenerated header for source-only consumption [\#4](https://github.com/devlooped/WebSocketChannel/issues/4) 38 | - Simplify source-only consumption by using explicit usings [\#3](https://github.com/devlooped/WebSocketChannel/issues/3) 39 | - 💟 Add SponsorLink to ensure ongoing maintenance [\#32](https://github.com/devlooped/WebSocketChannel/pull/32) (@kzu) 40 | 41 | :bug: Fixed bugs: 42 | 43 | - Added support for messages longer than 512 bytes. [\#25](https://github.com/devlooped/WebSocketChannel/pull/25) (@corradocavalli) 44 | 45 | :twisted_rightwards_arrows: Merged: 46 | 47 | - ⛙ ⬆️ Bump dependencies [\#34](https://github.com/devlooped/WebSocketChannel/pull/34) (@github-actions[bot]) 48 | - ⛙ ⬆️ Bump dependencies [\#33](https://github.com/devlooped/WebSocketChannel/pull/33) (@github-actions[bot]) 49 | 50 | ## [v0.9.2](https://github.com/devlooped/WebSocketChannel/tree/v0.9.2) (2021-10-15) 51 | 52 | [Full Changelog](https://github.com/devlooped/WebSocketChannel/compare/v0.9.1...v0.9.2) 53 | 54 | :hammer: Other: 55 | 56 | - Ensure single reader on channel [\#2](https://github.com/devlooped/WebSocketChannel/issues/2) 57 | 58 | ## [v0.9.1](https://github.com/devlooped/WebSocketChannel/tree/v0.9.1) (2021-10-04) 59 | 60 | [Full Changelog](https://github.com/devlooped/WebSocketChannel/compare/v0.9.0...v0.9.1) 61 | 62 | :hammer: Other: 63 | 64 | - Allow referencing directly from source [\#1](https://github.com/devlooped/WebSocketChannel/issues/1) 65 | 66 | ## [v0.9.0](https://github.com/devlooped/WebSocketChannel/tree/v0.9.0) (2021-10-04) 67 | 68 | [Full Changelog](https://github.com/devlooped/WebSocketChannel/compare/cb8103a2f18547e9697c0902c679e7578f0c8c65...v0.9.0) 69 | 70 | 71 | 72 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 73 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "allowPrerelease": true, 4 | "rollForward": "latestPatch", 5 | "version": "6.0.100-preview.*" 6 | } 7 | } -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Daniel Cazzulino and Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![Icon](https://raw.githubusercontent.com/devlooped/WebSocketChannel/main/assets/img/icon.png) WebSocketChannel 2 | ============ 3 | 4 | High-performance [System.Threading.Channels](https://devblogs.microsoft.com/dotnet/an-introduction-to-system-threading-channels/) API adapter for System.Net.WebSockets 5 | 6 | [![Version](https://img.shields.io/nuget/v/WebSocketChannel.svg?color=royalblue)](https://www.nuget.org/packages/WebSocketChannel) 7 | [![Downloads](https://img.shields.io/nuget/dt/WebSocketChannel.svg?color=green)](https://www.nuget.org/packages/WebSocketChannel) 8 | [![License](https://img.shields.io/github/license/devlooped/WebSocketChannel.svg?color=blue)](https://github.com/devlooped/WebSocketChannel/blob/main/license.txt) 9 | [![Build](https://img.shields.io/github/actions/workflow/status/devlooped/WebSocketChannel/build.yml?branch=main)](https://github.com/devlooped/WebSocketChannel/actions) 10 | 11 | 12 | # Usage 13 | 14 | ```csharp 15 | var client = new ClientWebSocket(); 16 | await client.ConnectAsync(serverUri, CancellationToken.None); 17 | 18 | Channel> channel = client.CreateChannel(); 19 | 20 | await channel.Writer.WriteAsync(Encoding.UTF8.GetBytes("hello").AsMemory()); 21 | 22 | // Read single message when it arrives 23 | ReadOnlyMemory response = await channel.Reader.ReadAsync(); 24 | 25 | // Read all messages while underlying websocket is open 26 | await foreach (var item in channel.Reader.ReadAllAsync()) 27 | { 28 | Console.WriteLine(Encoding.UTF8.GetString(item.Span)); 29 | } 30 | 31 | // Completing the writer closes the underlying websocket cleanly 32 | channel.Writer.Complete(); 33 | 34 | // Can also complete reporting an error for the remote party 35 | channel.Writer.Complete(new InvalidOperationException("Bad format")); 36 | ``` 37 | 38 | 39 | The `WebSocketChannel` can also be used on the server. The following example is basically 40 | taken from the documentation on [WebSockets in ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/websockets?view=aspnetcore-5.0#configure-the-middleware) 41 | and adapted to use a `WebSocketChannel` to echo messages to the client: 42 | 43 | ```csharp 44 | app.Use(async (context, next) => 45 | { 46 | if (context.Request.Path == "/ws") 47 | { 48 | if (context.WebSockets.IsWebSocketRequest) 49 | { 50 | using var webSocket = await context.WebSockets.AcceptWebSocketAsync(); 51 | var channel = WebSocketChannel.Create(webSocket); 52 | try 53 | { 54 | await foreach (var item in channel.Reader.ReadAllAsync(context.RequestAborted)) 55 | { 56 | await channel.Writer.WriteAsync(item, context.RequestAborted); 57 | } 58 | } 59 | catch (OperationCanceledException) 60 | { 61 | try 62 | { 63 | await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, null, default); 64 | } 65 | catch { } // Best effort to try closing cleanly. Client may be entirely gone. 66 | } 67 | } 68 | else 69 | { 70 | context.Response.StatusCode = (int) HttpStatusCode.BadRequest; 71 | } 72 | } 73 | else 74 | { 75 | await next(); 76 | } 77 | }); 78 | ``` 79 | 80 | 81 | # Installation 82 | 83 | This project can be used either as a regular nuget package: 84 | 85 | ``` 86 | 87 | ``` 88 | 89 | Or alternatively, referenced directly as a source-only dependency using [dotnet-file](https://www.nuget.org/packages/dotnet-file): 90 | 91 | ``` 92 | > dotnet file add https://github.com/devlooped/WebSocketChannel/blob/main/src/WebSocketChannel/WebSocketChannel.cs 93 | > dotnet file add https://github.com/devlooped/WebSocketChannel/blob/main/src/WebSocketChannel/WebSocketExtensions.cs 94 | ``` 95 | 96 | It's also possible to specify a desired target location for the referenced source files, such as: 97 | 98 | ``` 99 | > dotnet file add https://github.com/devlooped/WebSocketChannel/blob/main/src/WebSocketChannel/WebSocketChannel.cs src/MyProject/External/. 100 | > dotnet file add https://github.com/devlooped/WebSocketChannel/blob/main/src/WebSocketChannel/WebSocketExtensions.cs src/MyProject/External/. 101 | ``` 102 | 103 | When referenced as loose source files, it's easy to also get automated PRs when the upstream files change, 104 | as in the [dotnet-file.yml](https://github.com/devlooped/dotnet-file/blob/main/.github/workflows/dotnet-file.yml) workflow that 105 | keeps the repository up to date with a template. See also [dotnet-config](https://dotnetconfig.org), which is used to 106 | for the `dotnet-file` configuration settings that tracks all this. 107 | 108 | 109 | 110 | # Dogfooding 111 | 112 | [![CI Version](https://img.shields.io/endpoint?url=https://shields.kzu.app/vpre/WebSocketChannel/main&label=nuget.ci&color=brightgreen)](https://pkg.kzu.app/index.json) 113 | [![Build](https://github.com/devlooped/WebSocketChannel/workflows/build/badge.svg?branch=main)](https://github.com/devlooped/WebSocketChannel/actions) 114 | 115 | We also produce CI packages from branches and pull requests so you can dogfood builds as quickly as they are produced. 116 | 117 | The CI feed is `https://pkg.kzu.app/index.json`. 118 | 119 | The versioning scheme for packages is: 120 | 121 | - PR builds: *42.42.42-pr*`[NUMBER]` 122 | - Branch builds: *42.42.42-*`[BRANCH]`.`[COMMITS]` 123 | 124 | 125 | 126 | # Sponsors 127 | 128 | 129 | [![Clarius Org](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/clarius.png "Clarius Org")](https://github.com/clarius) 130 | [![MFB Technologies, Inc.](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/MFB-Technologies-Inc.png "MFB Technologies, Inc.")](https://github.com/MFB-Technologies-Inc) 131 | [![Torutek](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/torutek-gh.png "Torutek")](https://github.com/torutek-gh) 132 | [![DRIVE.NET, Inc.](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/drivenet.png "DRIVE.NET, Inc.")](https://github.com/drivenet) 133 | [![Keith Pickford](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/Keflon.png "Keith Pickford")](https://github.com/Keflon) 134 | [![Thomas Bolon](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/tbolon.png "Thomas Bolon")](https://github.com/tbolon) 135 | [![Kori Francis](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/kfrancis.png "Kori Francis")](https://github.com/kfrancis) 136 | [![Toni Wenzel](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/twenzel.png "Toni Wenzel")](https://github.com/twenzel) 137 | [![Uno Platform](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/unoplatform.png "Uno Platform")](https://github.com/unoplatform) 138 | [![Dan Siegel](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/dansiegel.png "Dan Siegel")](https://github.com/dansiegel) 139 | [![Reuben Swartz](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/rbnswartz.png "Reuben Swartz")](https://github.com/rbnswartz) 140 | [![Jacob Foshee](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/jfoshee.png "Jacob Foshee")](https://github.com/jfoshee) 141 | [![](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/Mrxx99.png "")](https://github.com/Mrxx99) 142 | [![Eric Johnson](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/eajhnsn1.png "Eric Johnson")](https://github.com/eajhnsn1) 143 | [![Ix Technologies B.V.](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/IxTechnologies.png "Ix Technologies B.V.")](https://github.com/IxTechnologies) 144 | [![David JENNI](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/davidjenni.png "David JENNI")](https://github.com/davidjenni) 145 | [![Jonathan ](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/Jonathan-Hickey.png "Jonathan ")](https://github.com/Jonathan-Hickey) 146 | [![Charley Wu](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/akunzai.png "Charley Wu")](https://github.com/akunzai) 147 | [![Ken Bonny](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/KenBonny.png "Ken Bonny")](https://github.com/KenBonny) 148 | [![Simon Cropp](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/SimonCropp.png "Simon Cropp")](https://github.com/SimonCropp) 149 | [![agileworks-eu](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/agileworks-eu.png "agileworks-eu")](https://github.com/agileworks-eu) 150 | [![sorahex](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/sorahex.png "sorahex")](https://github.com/sorahex) 151 | [![Zheyu Shen](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/arsdragonfly.png "Zheyu Shen")](https://github.com/arsdragonfly) 152 | [![Vezel](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/vezel-dev.png "Vezel")](https://github.com/vezel-dev) 153 | [![ChilliCream](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/ChilliCream.png "ChilliCream")](https://github.com/ChilliCream) 154 | [![4OTC](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/4OTC.png "4OTC")](https://github.com/4OTC) 155 | [![Vincent Limo](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/v-limo.png "Vincent Limo")](https://github.com/v-limo) 156 | [![Jordan S. Jones](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/jordansjones.png "Jordan S. Jones")](https://github.com/jordansjones) 157 | [![domischell](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/DominicSchell.png "domischell")](https://github.com/DominicSchell) 158 | 159 | 160 | 161 | 162 | [![Sponsor this project](https://raw.githubusercontent.com/devlooped/sponsors/main/sponsor.png "Sponsor this project")](https://github.com/sponsors/devlooped) 163 |   164 | 165 | [Learn more about GitHub Sponsors](https://github.com/sponsors) 166 | 167 | 168 | -------------------------------------------------------------------------------- /src/Benchmark/Benchmark.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | true 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Benchmark/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Net.WebSockets; 2 | using System.Text; 3 | using BenchmarkDotNet.Attributes; 4 | using BenchmarkDotNet.Diagnostics.Windows.Configs; 5 | using BenchmarkDotNet.Running; 6 | using Devlooped.Net; 7 | 8 | BenchmarkRunner.Run(); 9 | 10 | [NativeMemoryProfiler] 11 | [MemoryDiagnoser] 12 | public class Benchmarks 13 | { 14 | [Params(1000, 2000, 5000/*, 10000, 20000*/)] 15 | public int RunTime = 1000; 16 | 17 | [Benchmark] 18 | public async Task ReadAllBytes() 19 | { 20 | var cts = new CancellationTokenSource(RunTime); 21 | using var server = WebSocketServer.Create(); 22 | using var client = new ClientWebSocket(); 23 | await client.ConnectAsync(server.Uri, CancellationToken.None); 24 | var channel = client.CreateChannel(); 25 | 26 | try 27 | { 28 | _ = Task.Run(async () => 29 | { 30 | var mem = Encoding.UTF8.GetBytes(Guid.NewGuid().ToString()).AsMemory(); 31 | while (!cts.IsCancellationRequested) 32 | await channel.Writer.WriteAsync(mem); 33 | 34 | await server.DisposeAsync(); 35 | }); 36 | 37 | await foreach (var item in channel.Reader.ReadAllAsync(cts.Token)) 38 | { 39 | Console.WriteLine(Encoding.UTF8.GetString(item.Span)); 40 | } 41 | } 42 | catch (OperationCanceledException) 43 | { 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/CodeAnalysis/CodeAnalysis.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | WebSocketChannel.CodeAnalysis 6 | analyzers/dotnet/roslyn4.0 7 | 8 | 9 | 10 | $(MSBuildThisFileDirectory)..\SponsorLink\SponsorLink.Analyzer.targets 11 | 30 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/CodeAnalysis/WebSocketChannel.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | 7 | true 14 | 15 | 16 | 17 | 18 | $(CI) 19 | 20 | 21 | 22 | Daniel Cazzulino 23 | Copyright (C) Daniel Cazzulino and Contributors. All rights reserved. 24 | false 25 | MIT 26 | 27 | 28 | icon.png 29 | readme.md 30 | 31 | icon.png 32 | readme.md 33 | 34 | true 35 | true 36 | 37 | $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\bin')) 38 | 39 | 40 | true 41 | true 42 | 43 | 44 | true 45 | 46 | 47 | 48 | Release 49 | Latest 50 | 51 | 52 | false 53 | 54 | embedded 55 | true 56 | enable 57 | 58 | strict 59 | 60 | 61 | $(MSBuildProjectName) 62 | $(MSBuildProjectName.IndexOf('.')) 63 | $(MSBuildProjectName.Substring(0, $(RootNamespaceDot))) 64 | 65 | 66 | $(DefaultItemExcludes);*.binlog;*.zip;*.rsp;*.items;**/TestResults/**/*.* 67 | 68 | true 69 | true 70 | true 71 | true 72 | 73 | 74 | true 75 | 76 | 77 | false 78 | 79 | 80 | NU5105;$(NoWarn) 81 | 82 | true 83 | 84 | 85 | true 86 | 87 | 88 | LatestMinor 89 | 90 | 91 | 92 | 93 | $(MSBuildThisFileDirectory)kzu.snk 94 | 100 | 002400000480000094000000060200000024000052534131000400000100010051155fd0ee280be78d81cc979423f1129ec5dd28edce9cd94fd679890639cad54c121ebdb606f8659659cd313d3b3db7fa41e2271158dd602bb0039a142717117fa1f63d93a2d288a1c2f920ec05c4858d344a45d48ebd31c1368ab783596b382b611d8c92f9c1b3d338296aa21b12f3bc9f34de87756100c172c52a24bad2db 101 | 00352124762f2aa5 102 | true 103 | 104 | 105 | 106 | 114 | 42.42.42 115 | 116 | 117 | 118 | <_VersionLabel>$(VersionLabel.Replace('refs/heads/', '')) 119 | <_VersionLabel>$(_VersionLabel.Replace('refs/tags/v', '')) 120 | 121 | 122 | <_VersionLabel Condition="$(_VersionLabel.Contains('refs/pull/'))">$(VersionLabel.TrimEnd('.0123456789')) 123 | 124 | <_VersionLabel>$(_VersionLabel.Replace('refs/pull/', 'pr')) 125 | 126 | <_VersionLabel>$(_VersionLabel.Replace('/merge', '')) 127 | 128 | <_VersionLabel>$(_VersionLabel.Replace('/', '-')) 129 | 130 | 131 | $(_VersionLabel) 132 | 133 | $(_VersionLabel) 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 162 | 163 | 1.0.0 164 | $(VersionPrefix)-$(VersionSuffix) 165 | $(VersionPrefix) 166 | 167 | 168 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /src/Directory.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | WebSocketChannel 4 | 5 | -------------------------------------------------------------------------------- /src/SponsorLink/Analyzer/Analyzer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | SponsorableLib.Analyzers 5 | netstandard2.0 6 | true 7 | analyzers/dotnet/roslyn4.0 8 | true 9 | false 10 | true 11 | $(MSBuildThisFileDirectory)..\SponsorLink.Analyzer.targets 12 | disable 13 | SponsorableLib 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | $([System.IO.File]::ReadAllText('$(MSBuildThisFileDirectory)..\Tests\keys\kzu.pub.jwk')) 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/SponsorLink/Analyzer/GraceApiAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Linq; 3 | using Devlooped.Sponsors; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CSharp; 6 | using Microsoft.CodeAnalysis.Diagnostics; 7 | using static Devlooped.Sponsors.SponsorLink; 8 | 9 | namespace Analyzer; 10 | 11 | /// 12 | /// Links the sponsor status for the current compilation. 13 | /// 14 | [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] 15 | public class GraceApiAnalyzer : DiagnosticAnalyzer 16 | { 17 | public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( 18 | new DiagnosticDescriptor( 19 | "SL010", "Grace API usage", "Reports info for APIs that are in grace period", "Sponsors", 20 | DiagnosticSeverity.Info, true, helpLinkUri: Funding.HelpUrl), 21 | new DiagnosticDescriptor( 22 | "SL011", "Report Sponsoring Status", "Fake to get it to call us", "Sponsors", 23 | DiagnosticSeverity.Warning, true) 24 | ); 25 | 26 | #pragma warning disable RS1026 // Enable concurrent execution 27 | public override void Initialize(AnalysisContext context) 28 | #pragma warning restore RS1026 // Enable concurrent execution 29 | { 30 | #if !DEBUG 31 | // Only enable concurrent execution in release builds, otherwise debugging is quite annoying. 32 | context.EnableConcurrentExecution(); 33 | #endif 34 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); 35 | // Report info grace and expiring diagnostics. 36 | context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.InvocationExpression); 37 | context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.SimpleMemberAccessExpression); 38 | } 39 | 40 | void AnalyzeNode(SyntaxNodeAnalysisContext context) 41 | { 42 | var status = Diagnostics.GetOrSetStatus(() => context.Options); 43 | if (status != SponsorStatus.Grace) 44 | return; 45 | 46 | ReportGraceSymbol(context, context.Node.GetLocation(), context.SemanticModel.GetSymbolInfo(context.Node).Symbol); 47 | } 48 | 49 | void ReportGraceSymbol(SyntaxNodeAnalysisContext context, Location location, ISymbol? symbol) 50 | { 51 | if (symbol != null && 52 | symbol.GetAttributes().Any(attr => 53 | attr.AttributeClass?.ToDisplayString() == "System.ComponentModel.CategoryAttribute" && 54 | attr.ConstructorArguments.Any(arg => arg.Value as string == "Sponsored"))) 55 | { 56 | context.ReportDiagnostic(Diagnostic.Create( 57 | SupportedDiagnostics[0], 58 | location)); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/SponsorLink/Analyzer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "SponsorableLib": { 4 | "commandName": "DebugRoslynComponent", 5 | "targetProject": "..\\Tests\\Tests.csproj", 6 | "environmentVariables": { 7 | "SPONSORLINK_TRACE": "true" 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/SponsorLink/Analyzer/StatusReportingAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.IO; 4 | using System.Linq; 5 | using Devlooped.Sponsors; 6 | using Humanizer; 7 | using Microsoft.CodeAnalysis; 8 | using Microsoft.CodeAnalysis.Diagnostics; 9 | using Microsoft.CodeAnalysis.Text; 10 | using static Devlooped.Sponsors.SponsorLink; 11 | 12 | namespace Analyzer; 13 | 14 | [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] 15 | public class StatusReportingAnalyzer : DiagnosticAnalyzer 16 | { 17 | public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( 18 | new DiagnosticDescriptor( 19 | "SL001", "Report Sponsoring Status", "Reports sponsoring status determined by SponsorLink", "Sponsors", 20 | DiagnosticSeverity.Info, true), 21 | new DiagnosticDescriptor( 22 | "SL002", "Report Sponsoring Status", "Fake to get it to call us", "Sponsors", 23 | DiagnosticSeverity.Warning, true) 24 | ); 25 | 26 | public override void Initialize(AnalysisContext context) 27 | { 28 | context.EnableConcurrentExecution(); 29 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); 30 | 31 | context.RegisterCompilationAction(c => 32 | { 33 | var installed = c.Options.AdditionalFiles.Where(x => 34 | { 35 | var options = c.Options.AnalyzerConfigOptionsProvider.GetOptions(x); 36 | // In release builds, we'll have a single such item, since we IL-merge the analyzer. 37 | return options.TryGetValue("build_metadata.Analyzer.ItemType", out var itemType) && 38 | options.TryGetValue("build_metadata.Analyzer.NuGetPackageId", out var packageId) && 39 | itemType == "Analyzer" && 40 | packageId == "SponsorableLib"; 41 | }).Select(x => File.GetLastWriteTime(x.Path)).OrderByDescending(x => x).FirstOrDefault(); 42 | 43 | var status = Diagnostics.GetOrSetStatus(() => c.Options); 44 | 45 | var location = Location.None; 46 | if (c.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property.MSBuildProjectFullPath", out var value)) 47 | location = Location.Create(value, new TextSpan(), new LinePositionSpan()); 48 | 49 | c.ReportDiagnostic(Diagnostic.Create(SupportedDiagnostics[0], location, status.ToString())); 50 | 51 | if (installed != default) 52 | Tracing.Trace($"Status: {status}, Installed: {(DateTime.Now - installed).Humanize()} ago"); 53 | else 54 | Tracing.Trace($"Status: {status}, unknown install time"); 55 | }); 56 | } 57 | } -------------------------------------------------------------------------------- /src/SponsorLink/Analyzer/StatusReportingGenerator.cs: -------------------------------------------------------------------------------- 1 | using Devlooped.Sponsors; 2 | using Microsoft.CodeAnalysis; 3 | using static Devlooped.Sponsors.SponsorLink; 4 | 5 | namespace Analyzer; 6 | 7 | [Generator] 8 | public class StatusReportingGenerator : IIncrementalGenerator 9 | { 10 | public void Initialize(IncrementalGeneratorInitializationContext context) 11 | { 12 | context.RegisterSourceOutput( 13 | // this is required to ensure status is registered properly independently 14 | // of analyzer runs. 15 | context.GetStatusOptions(), 16 | (spc, source) => 17 | { 18 | var status = Diagnostics.GetOrSetStatus(source); 19 | spc.AddSource("StatusReporting.cs", 20 | $""" 21 | // Status: {status} 22 | // DesignTimeBuild: {source.GlobalOptions.IsDesignTimeBuild()} 23 | """); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/SponsorLink/Analyzer/buildTransitive/SponsorableLib.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/SponsorLink/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | latest 6 | true 7 | annotations 8 | true 9 | 10 | false 11 | $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)bin')) 12 | 13 | https://pkg.kzu.app/index.json;https://api.nuget.org/v3/index.json 14 | $(PackageOutputPath);$(RestoreSources) 15 | 16 | 18 | $([System.DateTime]::Parse("2024-03-15")) 19 | $([System.DateTime]::UtcNow.Subtract($(Epoc)).TotalDays) 20 | $([System.Math]::Truncate($(TotalDays))) 21 | $([System.Math]::Floor($([MSBuild]::Divide($([System.DateTime]::UtcNow.TimeOfDay.TotalSeconds), 10)))) 22 | 42.$(Days).$(Seconds) 23 | 24 | SponsorableLib 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/SponsorLink/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/SponsorLink/Library/Library.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | SponsorableLib 5 | netstandard2.0 6 | true 7 | SponsorableLib 8 | Sample library incorporating SponsorLink checks 9 | true 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/SponsorLink/Library/MyClass.cs: -------------------------------------------------------------------------------- 1 | namespace SponsorableLib; 2 | 3 | public class MyClass 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /src/SponsorLink/Library/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Bar 122 | 123 | -------------------------------------------------------------------------------- /src/SponsorLink/Library/readme.md: -------------------------------------------------------------------------------- 1 | # Sponsorable Library 2 | 3 | Example of a library that is available for sponsorship and leverages 4 | [SponsorLink](https://github.com/devlooped/SponsorLink) to remind users 5 | in an IDE (VS/Rider). 6 | -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink.Analyzer.Tests.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | $([MSBuild]::ValueOrDefault('%(FullPath)', '').Replace('net6.0', 'netstandard2.0').Replace('net8.0', 'netstandard2.0').Replace('netcoreapp3.1', 'netstandard2.0')) 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink/AnalyzerOptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.Diagnostics; 2 | 3 | static class AnalyzerOptionsExtensions 4 | { 5 | /// 6 | /// Gets whether the current build is a design-time build. 7 | /// 8 | public static bool IsDesignTimeBuild(this AnalyzerConfigOptionsProvider options) => 9 | options.GlobalOptions.TryGetValue("build_property.DesignTimeBuild", out var value) && 10 | bool.TryParse(value, out var isDesignTime) && isDesignTime; 11 | 12 | /// 13 | /// Gets whether the current build is a design-time build. 14 | /// 15 | public static bool IsDesignTimeBuild(this AnalyzerConfigOptions options) => 16 | options.TryGetValue("build_property.DesignTimeBuild", out var value) && 17 | bool.TryParse(value, out var isDesignTime) && isDesignTime; 18 | } -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink/AppDomainDictionary.cs: -------------------------------------------------------------------------------- 1 | // 2 | #nullable enable 3 | using System; 4 | 5 | namespace Devlooped.Sponsors; 6 | 7 | /// 8 | /// A helper class to store and retrieve values from the current 9 | /// as typed named values. 10 | /// 11 | /// 12 | /// This allows tools that run within the same app domain to share state, such as 13 | /// MSBuild tasks or Roslyn analyzers. 14 | /// 15 | static class AppDomainDictionary 16 | { 17 | /// 18 | /// Gets the value associated with the specified name, or creates a new one if it doesn't exist. 19 | /// 20 | public static TValue Get(string name) where TValue : notnull, new() 21 | { 22 | var data = AppDomain.CurrentDomain.GetData(name); 23 | if (data is TValue firstTry) 24 | return firstTry; 25 | 26 | lock (AppDomain.CurrentDomain) 27 | { 28 | if (AppDomain.CurrentDomain.GetData(name) is TValue secondTry) 29 | return secondTry; 30 | 31 | var newValue = new TValue(); 32 | AppDomain.CurrentDomain.SetData(name, newValue); 33 | return newValue; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink/Resources.es-AR.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Patrocinar los proyectos en que dependés asegura que se mantengan activos, y que recibas el apoyo que necesitás. También es muy económico y está disponible en todo el mundo! 122 | Por favor considerá apoyar el proyecto patrocinando en {0} y ejecutando posteriormente 'sponsor sync {1}'. 123 | 124 | 125 | Por favor considerá apoyar {0} patrocinando {1} 🙏 126 | 127 | 128 | Estado de patrocinio desconocido 129 | 130 | 131 | Funcionalidades exclusivas para patrocinadores pueden no estar disponibles. Ejecutá 'sponsor sync {0}' y, opcionalmente, habilita la sincronización automática. 132 | 133 | 134 | El estado de patrocino ha expirado y la sincronización automática no está habilitada. 135 | 136 | 137 | El estado de patrocino ha expirado 138 | 139 | 140 | Sos un verdadero héroe. Tu patrocinio ayuda a mantener el proyecto vivo y próspero 🙏. 141 | 142 | 143 | Gracias por apoyar a {0} con tu patrocinio 💟! 144 | 145 | 146 | Sos un patrocinador del proyecto, sos lo máximo 💟! 147 | 148 | 149 | El estado de patrocino ha expirado y estás en un período de gracia. Ejecutá 'sponsor sync {0}' y, opcionalmente, habilitá la sincronización automática. 150 | 151 | 152 | El estado de patrocino necesita actualización periódica y la sincronización automática no está habilitada. 153 | 154 | 155 | El estado de patrocino ha expirado y el período de gracia terminará pronto 156 | 157 | 158 | y 159 | 160 | 161 | o 162 | 163 | 164 | Gracias por ser parte del equipo por tu contribución 🙏. 165 | 166 | 167 | Gracias por ser parte del equipo {0} con tu contribución 💟! 168 | 169 | 170 | Sos un contribuidor al proyecto, sos groso 💟! 171 | 172 | 173 | Patrocinar los proyectos en que dependés asegura que se mantengan activos, y que recibas el apoyo que necesitás. También es muy económico y está disponible en todo el mundo! 174 | Por favor considerá apoyar el proyecto patrocinando en {0} y ejecutando posteriormente 'sponsor sync {1}'. 175 | 176 | 177 | El período de prueba finaliza en {0} día(s). Disfrutá y por favor considerá apoyar {1} patrocinando {2} 🙏 178 | 179 | 180 | Estado de patrocinio desconocido, período de prueba 181 | 182 | 183 | /// <remarks> 184 | /// GitHub Sponsors es una excelente manera de apoyar proyectos de código abierto, y está disponible en la mayor parte del mundo. 185 | /// Sos considerado sponsor si: 186 | /// - Bancás directamente a Devlooped 💪. 187 | /// - Contribuiste con código a cualquiera de los proyectos de Devlooped. 188 | /// - Pertenecés a una organización de GitHub que está patrocinando a Devlooped. 189 | /// - Contribuiste a nugets activos y populares que son open-source. 190 | /// 191 | /// En este caso, simplemente ejecutá: 192 | /// > dotnet tool install -g dotnet-sponsor; sponsor sync devlooped 193 | /// 194 | /// Y el atributo [Obsolete] vuela de una. Gracias totales! 🙏 195 | /// </remarks> 196 | 197 | 198 | /// <remarks> 199 | /// Esta API requiere patrocinio. Tu período de yapa termina en {0} día(s). 200 | /// 201 | /// GitHub Sponsors es una excelente manera de apoyar proyectos de código abierto, y está disponible en la mayor parte del mundo. 202 | /// Sos considerado sponsor si: 203 | /// - Bancás directamente a Devlooped 💪. 204 | /// - Contribuiste con código a cualquiera de los proyectos de Devlooped. 205 | /// - Pertenecés a una organización de GitHub que está patrocinando a Devlooped. 206 | /// - Contribuiste a nugets activos y populares que son open-source. 207 | /// 208 | /// En este caso, simplemente ejecutá: 209 | /// > dotnet tool install -g dotnet-sponsor; sponsor sync devlooped 210 | /// 211 | /// ¡Gracias totales! 🙏 212 | /// </remarks> 213 | 214 | -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink/Resources.es.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Patrocinar los proyectos en que dependes asegura que se mantengan activos, y que recibas el apoyo que necesitas. También es muy económico y está disponible en todo el mundo! 122 | Por favor considera apoyar el proyecto patrocinando en {0} y ejecutando posteriormente 'sponsor sync {1}'. 123 | 124 | 125 | Por favor considere apoyar {0} patrocinando {1} 🙏 126 | 127 | 128 | Estado de patrocinio desconocido 129 | 130 | 131 | Funcionalidades exclusivas para patrocinadores pueden no estar disponibles. Ejecuta 'sponsor sync {0}' y, opcionalmente, habilita la sincronización automática. 132 | 133 | 134 | El estado de patrocino ha expirado y la sincronización automática no está habilitada. 135 | 136 | 137 | El estado de patrocino ha expirado 138 | 139 | 140 | Eres un verdadero héroe. Tu patrocinio ayuda a mantener el proyecto vivo y próspero 🙏. 141 | 142 | 143 | Gracias por apoyar a {0} con tu patrocinio 💟! 144 | 145 | 146 | Eres un patrocinador del proyecto, eres lo máximo 💟! 147 | 148 | 149 | El estado de patrocino ha expirado y estás en un período de gracia. Ejecuta 'sponsor sync {0}' y, opcionalmente, habilita la sincronización automática. 150 | 151 | 152 | El estado de patrocino necesita actualización periódica y la sincronización automática no está habilitada. 153 | 154 | 155 | El estado de patrocino ha expirado y el período de gracia terminará pronto 156 | 157 | 158 | y 159 | 160 | 161 | o 162 | 163 | 164 | Gracias por ser parte del equipo por tu contribución 🙏. 165 | 166 | 167 | Gracias por ser parte del equipo {0} con tu contribución 💟! 168 | 169 | 170 | Eres un contribuidor al proyecto, eres lo máximo 💟! 171 | 172 | 173 | El uso de {0} sin warnings en el editor requiere un patrocinio activo. Ver mas en {1}. 174 | 175 | 176 | Patrocinar los proyectos en que dependes asegura que se mantengan activos, y que recibas el apoyo que necesitas. También es muy económico y está disponible en todo el mundo! 177 | Por favor considera apoyar el proyecto patrocinando en {0} y ejecutando posteriormente 'sponsor sync {1}'. 178 | 179 | 180 | El período de prueba finaliza en {0} día(s). Disfrute y por favor considere apoyar {1} patrocinando {2} 🙏 181 | 182 | 183 | Estado de patrocinio desconocido, período de prueba 184 | 185 | 186 | /// <remarks> 187 | /// GitHub Sponsors es una excelente manera de apoyar proyectos de código abierto, y está disponible en la mayor parte del mundo. 188 | /// Se te considera un patrocinador si: 189 | /// - Estás patrocinando directamente a Devlooped. 190 | /// - Has contribuido con código a cualquiera de los proyectos de Devlooped. 191 | /// - Perteneces a una organización de GitHub que está patrocinando a Devlooped. 192 | /// - Has contribuido a nugets activos y populares que son de código abierto. 193 | /// 194 | /// Si es así, simplemente ejecuta: 195 | /// > dotnet tool install -g dotnet-sponsor; sponsor sync devlooped 196 | /// 197 | /// Posteriormente, el atributo [Obsolete] será eliminado. 198 | /// ¡Gracias! 🙏 199 | /// </remarks> 200 | 201 | 202 | /// <remarks> 203 | /// Esta API requiere patrocinio. Su período de gracia termina en {0} día(s). 204 | /// 205 | /// GitHub Sponsors es una excelente manera de apoyar proyectos de código abierto, y está disponible en la mayor parte del mundo. 206 | /// Se te considera un patrocinador si: 207 | /// - Estás patrocinando directamente a Devlooped. 208 | /// - Has contribuido con código a cualquiera de los proyectos de Devlooped. 209 | /// - Perteneces a una organización de GitHub que está patrocinando a Devlooped. 210 | /// - Has contribuido a packetes en nuget.org activos y populares que son de código abierto 211 | /// 212 | /// Si es así, simplemente ejecuta: 213 | /// > dotnet tool install -g dotnet-sponsor; sponsor sync devlooped 214 | /// 215 | /// ¡Gracias! 🙏 216 | /// </remarks> 217 | 218 | 219 | Gracias por ser parte de la comunidad de código abierto con tus contribuciones 🙏. 220 | 221 | 222 | Gracias por ser autor de código abierto 💟! 223 | 224 | 225 | Sos un autor de código abierto, eres lo máximo 💟! 226 | 227 | -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Sponsoring projects you depend on ensures they remain active, and that you get the support you need. It's also super affordable and available worldwide! 122 | Please consider supporting the project by sponsoring at {0} and running 'sponsor sync {1}' afterwards. 123 | Unknown sponsor description 124 | 125 | 126 | Please consider supporting {0} by sponsoring {1} 🙏 127 | 128 | 129 | Unknown sponsor status 130 | 131 | 132 | Sponsor-only features may be disabled. Please run 'sponsor sync {0}' and optionally enable automatic sync. 133 | 134 | 135 | Sponsor status has expired and automatic sync has not been enabled. 136 | 137 | 138 | Sponsor status expired 139 | 140 | 141 | You are a true hero. Your sponsorship helps keep the project alive and thriving 🙏. 142 | 143 | 144 | Thank you for supporting {0} with your sponsorship 💟! 145 | 146 | 147 | You are a sponsor of the project, you rock 💟! 148 | 149 | 150 | Sponsor status has expired and you are in the grace period. Please run 'sponsor sync {0}' and optionally enable automatic sync. 151 | 152 | 153 | Sponsor status needs periodic updating and automatic sync has not been enabled. 154 | 155 | 156 | Sponsor status expired, grace period ending soon 157 | 158 | 159 | and 160 | 161 | 162 | or 163 | 164 | 165 | Thanks for being part of the team with your contributions 🙏. 166 | 167 | 168 | Thank you for being part of team {0} with your contributions 💟! 169 | 170 | 171 | You are a contributor to the project, you rock 💟! 172 | 173 | 174 | Editor usage of {0} without warnings requires an active sponsorship. Learn more at {1}. 175 | 176 | 177 | Sponsoring projects you depend on ensures they remain active, and that you get the support you need. It's also super affordable and available worldwide! 178 | Please consider supporting the project by sponsoring at {0} and running 'sponsor sync {1}' afterwards. 179 | 180 | 181 | Grace period ends in {0} days. Enjoy and please consider supporting {1} by sponsoring {2} 🙏 182 | 183 | 184 | Unknown sponsor status, grace period 185 | 186 | 187 | /// <remarks> 188 | /// GitHub Sponsors is a great way to support open source projects, and it's available throughout most of the world. 189 | /// You are considered a sponsor if: 190 | /// - You are directly sponsoring Devlooped 191 | /// - You contributed code to any of Devlooped's projects. 192 | /// - You belong to a GitHub organization that is sponsoring Devlooped. 193 | /// - You contributed to active and popular nuget packages that are OSS. 194 | /// 195 | /// If so, just run: 196 | /// > dotnet tool install -g dotnet-sponsor; sponsor sync devlooped 197 | /// 198 | /// Subsequently, the [Obsolete] attribute will be removed. 199 | /// Thanks! 🙏 200 | /// </remarks> 201 | 202 | 203 | /// <remarks> 204 | /// This is a sponsored API. Your grace period will expire in {0} day(s). 205 | /// 206 | /// GitHub Sponsors is a great way to support open source projects, and it's available throughout most of the world. 207 | /// You are considered a sponsor if: 208 | /// - You are directly sponsoring Devlooped 209 | /// - You contributed code to any of Devlooped's projects. 210 | /// - You belong to a GitHub organization that is sponsoring Devlooped. 211 | /// - You contributed to active and popular nuget packages that are OSS. 212 | /// 213 | /// If so, just run: 214 | /// > dotnet tool install -g dotnet-sponsor; sponsor sync devlooped 215 | /// 216 | /// Thanks! 🙏 217 | /// </remarks> 218 | 219 | 220 | Thanks for being part of the open source community with your contributions 🙏. 221 | 222 | 223 | Thank you for being an open source author 💟! 224 | 225 | 226 | You are a an open source author, you rock 💟! 227 | 228 | -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink/SponsorLink.cs: -------------------------------------------------------------------------------- 1 | // 2 | #nullable enable 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Collections.Immutable; 6 | using System.Diagnostics; 7 | using System.Diagnostics.CodeAnalysis; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Reflection; 11 | using System.Security.Claims; 12 | using Microsoft.CodeAnalysis; 13 | using Microsoft.CodeAnalysis.Diagnostics; 14 | using Microsoft.IdentityModel.JsonWebTokens; 15 | using Microsoft.IdentityModel.Tokens; 16 | 17 | namespace Devlooped.Sponsors; 18 | 19 | static partial class SponsorLink 20 | { 21 | public record StatusOptions(ImmutableArray AdditionalFiles, AnalyzerConfigOptions GlobalOptions); 22 | 23 | /// 24 | /// Statically cached dictionary of sponsorable accounts and their public key (in JWK format), 25 | /// retrieved from assembly metadata attributes starting with "Funding.GitHub.". 26 | /// 27 | public static Dictionary Sponsorables { get; } = typeof(SponsorLink).Assembly 28 | .GetCustomAttributes() 29 | .Where(x => x.Key.StartsWith("Funding.GitHub.")) 30 | .Select(x => new { Key = x.Key[15..], x.Value }) 31 | .ToDictionary(x => x.Key, x => x.Value); 32 | 33 | /// 34 | /// Whether the current process is running in an IDE, either 35 | /// or . 36 | /// 37 | public static bool IsEditor => IsVisualStudio || IsRider; 38 | 39 | /// 40 | /// Whether the current process is running as part of an active Visual Studio instance. 41 | /// 42 | public static bool IsVisualStudio => 43 | Environment.GetEnvironmentVariable("ServiceHubLogSessionKey") != null || 44 | Environment.GetEnvironmentVariable("VSAPPIDNAME") != null; 45 | 46 | /// 47 | /// Whether the current process is running as part of an active Rider instance. 48 | /// 49 | public static bool IsRider => 50 | Environment.GetEnvironmentVariable("RESHARPER_FUS_SESSION") != null || 51 | Environment.GetEnvironmentVariable("IDEA_INITIAL_DIRECTORY") != null; 52 | 53 | /// 54 | /// A unique session ID associated with the current IDE or process running the analyzer. 55 | /// 56 | public static string SessionId => 57 | IsVisualStudio ? Environment.GetEnvironmentVariable("ServiceHubLogSessionKey") : 58 | IsRider ? Environment.GetEnvironmentVariable("RESHARPER_FUS_SESSION") : 59 | Process.GetCurrentProcess().Id.ToString(); 60 | 61 | /// 62 | /// Manages the sharing and reporting of diagnostics across the source generator 63 | /// and the diagnostic analyzer, to avoid doing the online check more than once. 64 | /// 65 | public static DiagnosticsManager Diagnostics { get; } = new(); 66 | 67 | /// 68 | /// Gets the expiration date from the principal, if any. 69 | /// 70 | /// 71 | /// Whichever "exp" claim is the latest, or if none found. 72 | /// 73 | public static DateTime? GetExpiration(this ClaimsPrincipal principal) 74 | // get all "exp" claims, parse them and return the latest one or null if none found 75 | => principal.FindAll("exp") 76 | .Select(c => c.Value) 77 | .Select(long.Parse) 78 | .Select(DateTimeOffset.FromUnixTimeSeconds) 79 | .Max().DateTime is var exp && exp == DateTime.MinValue ? null : exp; 80 | 81 | /// 82 | /// Gets all necessary additional files to determine status. 83 | /// 84 | public static ImmutableArray GetSponsorAdditionalFiles(this AnalyzerOptions? options) 85 | => options == null ? ImmutableArray.Create() : options.AdditionalFiles 86 | .Where(x => x.IsSponsorManifest(options.AnalyzerConfigOptionsProvider) || x.IsSponsorableAnalyzer(options.AnalyzerConfigOptionsProvider)) 87 | .ToImmutableArray(); 88 | 89 | /// 90 | /// Gets all sponsor manifests from the provided analyzer options. 91 | /// 92 | public static IncrementalValueProvider> GetSponsorAdditionalFiles(this IncrementalGeneratorInitializationContext context) 93 | => context.AdditionalTextsProvider.Combine(context.AnalyzerConfigOptionsProvider) 94 | .Where(source => 95 | { 96 | var (text, provider) = source; 97 | return text.IsSponsorManifest(provider) || text.IsSponsorableAnalyzer(provider); 98 | }) 99 | .Select((source, c) => source.Left) 100 | .Collect(); 101 | 102 | /// 103 | /// Gets the status options for use within an incremental generator, to avoid depending on 104 | /// analyzer runs. Used in combination with . 105 | /// 106 | public static IncrementalValueProvider GetStatusOptions(this IncrementalGeneratorInitializationContext context) 107 | => context.GetSponsorAdditionalFiles().Combine(context.AnalyzerConfigOptionsProvider) 108 | .Select((source, _) => new StatusOptions(source.Left, source.Right.GlobalOptions)); 109 | 110 | /// 111 | /// Gets the status options for use within a source generator, to avoid depending on 112 | /// analyzer runs. Used in combination with . 113 | /// 114 | public static StatusOptions GetStatusOptions(this GeneratorExecutionContext context) 115 | => new StatusOptions( 116 | context.AdditionalFiles.Where(x => x.IsSponsorManifest(context.AnalyzerConfigOptions) || x.IsSponsorableAnalyzer(context.AnalyzerConfigOptions)).ToImmutableArray(), 117 | context.AnalyzerConfigOptions.GlobalOptions); 118 | 119 | static bool IsSponsorManifest(this AdditionalText text, AnalyzerConfigOptionsProvider provider) 120 | => provider.GetOptions(text).TryGetValue("build_metadata.SponsorManifest.ItemType", out var itemType) && 121 | itemType == "SponsorManifest" && 122 | Sponsorables.ContainsKey(Path.GetFileNameWithoutExtension(text.Path)); 123 | 124 | static bool IsSponsorableAnalyzer(this AdditionalText text, AnalyzerConfigOptionsProvider provider) 125 | => provider.GetOptions(text) is { } options && 126 | options.TryGetValue("build_metadata.Analyzer.ItemType", out var itemType) && 127 | options.TryGetValue("build_metadata.Analyzer.NuGetPackageId", out var packageId) && 128 | itemType == "Analyzer" && 129 | Funding.PackageIds.Contains(packageId); 130 | 131 | /// 132 | /// Reads all manifests, validating their signatures. 133 | /// 134 | /// The combined principal with all identities (and their claims) from each provided and valid JWT 135 | /// The tokens to read and their corresponding JWK for signature verification. 136 | /// if at least one manifest can be successfully read and is valid. 137 | /// otherwise. 138 | public static bool TryRead([NotNullWhen(true)] out ClaimsPrincipal? principal, params (string jwt, string jwk)[] values) 139 | => TryRead(out principal, values.AsEnumerable()); 140 | 141 | /// 142 | /// Reads all manifests, validating their signatures. 143 | /// 144 | /// The combined principal with all identities (and their claims) from each provided and valid JWT 145 | /// The tokens to read and their corresponding JWK for signature verification. 146 | /// if at least one manifest can be successfully read and is valid. 147 | /// otherwise. 148 | public static bool TryRead([NotNullWhen(true)] out ClaimsPrincipal? principal, IEnumerable<(string jwt, string jwk)> values) 149 | { 150 | principal = null; 151 | 152 | foreach (var value in values) 153 | { 154 | if (string.IsNullOrWhiteSpace(value.jwt) || string.IsNullOrEmpty(value.jwk)) 155 | continue; 156 | 157 | if (Validate(value.jwt, value.jwk, out var token, out var identity, false) == ManifestStatus.Valid && identity != null) 158 | { 159 | if (principal == null) 160 | principal = new JwtRolesPrincipal(identity); 161 | else 162 | principal.AddIdentity(identity); 163 | } 164 | } 165 | 166 | return principal != null; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink/SponsorLink.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | SponsorLink 6 | disable 7 | false 8 | CoreResGen;$(CoreCompileDependsOn) 9 | SponsorLink 10 | 11 | 12 | 13 | 14 | $(Product) 15 | $(PackageId) 16 | 17 | $([System.Text.RegularExpressions.Regex]::Replace("$(FundingProduct)", "[^A-Z]", "")) 18 | 19 | 21 20 | 21 | https://github.com/devlooped#sponsorlink 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | $(FundingProduct) 49 | 50 | 51 | 52 | 53 | 54 | 55 | <_FundingAnalyzerPackageId Include="@(FundingAnalyzerPackageId -> '"%(Identity)"')" /> 56 | 57 | 58 | <_FundingPackageIds>@(_FundingAnalyzerPackageId, ',') 59 | 60 | 61 | 62 | using System.Collections.Generic%3B 63 | 64 | namespace Devlooped.Sponsors%3B 65 | 66 | partial class SponsorLink 67 | { 68 | public partial class Funding 69 | { 70 | public static HashSet<string> PackageIds { get%3B } = [$(_FundingPackageIds)]%3B 71 | public const string Product = "$(FundingProduct)"%3B 72 | public const string Prefix = "$(FundingPrefix)"%3B 73 | public const string HelpUrl = "$(FundingHelpUrl)"%3B 74 | public const int Grace = $(FundingGrace)%3B 75 | } 76 | } 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | $([System.IO.File]::ReadAllText('$(MSBuildProjectDirectory)\$(BaseIntermediateOutputPath)devlooped.jwk')) 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs: -------------------------------------------------------------------------------- 1 | // 2 | #nullable enable 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using Microsoft.CodeAnalysis; 6 | using Microsoft.CodeAnalysis.Diagnostics; 7 | using static Devlooped.Sponsors.SponsorLink; 8 | 9 | namespace Devlooped.Sponsors; 10 | 11 | /// 12 | /// Links the sponsor status for the current compilation. 13 | /// 14 | [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] 15 | public class SponsorLinkAnalyzer : DiagnosticAnalyzer 16 | { 17 | public override ImmutableArray SupportedDiagnostics { get; } = DiagnosticsManager.KnownDescriptors.Values.ToImmutableArray(); 18 | 19 | #pragma warning disable RS1026 // Enable concurrent execution 20 | public override void Initialize(AnalysisContext context) 21 | #pragma warning restore RS1026 // Enable concurrent execution 22 | { 23 | #if !DEBUG 24 | // Only enable concurrent execution in release builds, otherwise debugging is quite annoying. 25 | context.EnableConcurrentExecution(); 26 | #endif 27 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); 28 | 29 | #pragma warning disable RS1013 // Start action has no registered non-end actions 30 | // We do this so that the status is set at compilation start so we can use it 31 | // across all other analyzers. We report only on finish because multiple 32 | // analyzers can report the same diagnostic and we want to avoid duplicates. 33 | context.RegisterCompilationStartAction(ctx => 34 | { 35 | // Setting the status early allows other analyzers to potentially check for it. 36 | var status = Diagnostics.GetOrSetStatus(() => ctx.Options); 37 | 38 | // Never report any diagnostic unless we're in an editor. 39 | if (IsEditor) 40 | { 41 | // NOTE: for multiple projects with the same product name, we only report one diagnostic, 42 | // so it's expected to NOT get a diagnostic back. Also, we don't want to report 43 | // multiple diagnostics for each project in a solution that uses the same product. 44 | ctx.RegisterCompilationEndAction(ctx => 45 | { 46 | // We'd never report Info/hero link if users opted out of it. 47 | if (status.IsSponsor() && 48 | ctx.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property.SponsorLinkHero", out var slHero) && 49 | bool.TryParse(slHero, out var isHero) && isHero) 50 | return; 51 | 52 | // Only report if the package is directly referenced in the project for 53 | // any of the funding packages we monitor (i.e. we could have one or more 54 | // metapackages we also consider "direct references). 55 | // See SL_CollectDependencies in buildTransitive\Devlooped.Sponsors.targets 56 | foreach (var prop in Funding.PackageIds.Select(id => id.Replace('.', '_'))) 57 | { 58 | if (ctx.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property." + prop, out var package) && 59 | package?.Length > 0 && 60 | Diagnostics.TryGet() is { } diagnostic) 61 | { 62 | ctx.ReportDiagnostic(diagnostic); 63 | break; 64 | } 65 | } 66 | }); 67 | } 68 | }); 69 | #pragma warning restore RS1013 // Start action has no registered non-end actions 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink/SponsorManifest.cs: -------------------------------------------------------------------------------- 1 | // 2 | #nullable enable 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics.CodeAnalysis; 6 | using System.IO; 7 | using System.Security.Claims; 8 | using Microsoft.IdentityModel.JsonWebTokens; 9 | using Microsoft.IdentityModel.Tokens; 10 | 11 | namespace Devlooped.Sponsors; 12 | 13 | /// 14 | /// The resulting status from validation. 15 | /// 16 | public enum ManifestStatus 17 | { 18 | /// 19 | /// The manifest couldn't be read at all. 20 | /// 21 | Unknown, 22 | /// 23 | /// The manifest was read and is valid (not expired and properly signed). 24 | /// 25 | Valid, 26 | /// 27 | /// The manifest was read but has expired. 28 | /// 29 | Expired, 30 | /// 31 | /// The manifest was read, but its signature is invalid. 32 | /// 33 | Invalid, 34 | } 35 | 36 | /// 37 | /// Represents the sponsorship status of a user. 38 | /// 39 | /// The status. 40 | /// The principal potentially containing roles validated from the manifest. 41 | /// The security token from the validated manifest. 42 | public record SponsorManifest(ManifestStatus Status, ClaimsPrincipal Principal, SecurityToken? SecurityToken) 43 | { 44 | /// 45 | /// Whether the manifest is . 46 | /// 47 | public bool IsValid => Status == ManifestStatus.Valid; 48 | } 49 | 50 | static partial class SponsorLink 51 | { 52 | /// 53 | /// Reads the local manifest (if present) for the specified sponsorable account and validates it 54 | /// against the given JWK key. 55 | /// 56 | /// The sponsorable account to read. 57 | /// The public key to validate the signature on the manifest JWT if found. 58 | /// Whether to validate the manifest expiration. If , 59 | /// an expired manifest will be reported as . The expiration date 60 | /// can be checked in that case via the . 61 | /// A manifest that represents the user status. 62 | public static SponsorManifest GetManifest(string sponsorable, string jwk, bool validateExpiration = true) 63 | { 64 | var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), 65 | ".sponsorlink", "github", sponsorable + ".jwt"); 66 | 67 | if (!File.Exists(path)) 68 | return new SponsorManifest(ManifestStatus.Unknown, new ClaimsPrincipal(), null); 69 | 70 | return ParseManifest(File.ReadAllText(path), jwk, validateExpiration); 71 | } 72 | 73 | internal static SponsorManifest ParseManifest(string jwt, string jwk, bool validateExpiration) 74 | { 75 | var status = Validate(jwt, jwk, out var token, out var identity, validateExpiration); 76 | 77 | if (status == ManifestStatus.Unknown || identity == null) 78 | return new SponsorManifest(status, new ClaimsPrincipal(), token); 79 | 80 | return new SponsorManifest(status, new JwtRolesPrincipal(identity), token); 81 | } 82 | 83 | /// 84 | /// Validates the manifest signature and optional expiration. 85 | /// 86 | /// The JWT to validate. 87 | /// The key to validate the manifest signature with. 88 | /// Except when returning , returns the security token read from the JWT, even if signature check failed. 89 | /// The associated claims, only when return value is not . 90 | /// Whether to check for expiration. 91 | /// The status of the validation. 92 | public static ManifestStatus Validate(string jwt, string jwk, out SecurityToken? token, out ClaimsIdentity? identity, bool validateExpiration) 93 | { 94 | token = default; 95 | identity = default; 96 | 97 | SecurityKey key; 98 | try 99 | { 100 | key = JsonWebKey.Create(jwk); 101 | } 102 | catch (ArgumentException) 103 | { 104 | return ManifestStatus.Unknown; 105 | } 106 | 107 | var handler = new JsonWebTokenHandler { MapInboundClaims = false }; 108 | 109 | if (!handler.CanReadToken(jwt)) 110 | return ManifestStatus.Unknown; 111 | 112 | var validation = new TokenValidationParameters 113 | { 114 | RequireExpirationTime = false, 115 | ValidateLifetime = false, 116 | ValidateAudience = false, 117 | ValidateIssuer = false, 118 | ValidateIssuerSigningKey = true, 119 | IssuerSigningKey = key, 120 | RoleClaimType = "roles", 121 | NameClaimType = "sub", 122 | }; 123 | 124 | var result = handler.ValidateTokenAsync(jwt, validation).Result; 125 | if (!result.IsValid || result.Exception != null) 126 | { 127 | if (result.Exception is SecurityTokenInvalidSignatureException) 128 | { 129 | var jwtToken = handler.ReadJsonWebToken(jwt); 130 | token = jwtToken; 131 | identity = new ClaimsIdentity(jwtToken.Claims); 132 | return ManifestStatus.Invalid; 133 | } 134 | else 135 | { 136 | var jwtToken = handler.ReadJsonWebToken(jwt); 137 | token = jwtToken; 138 | identity = new ClaimsIdentity(jwtToken.Claims); 139 | return ManifestStatus.Invalid; 140 | } 141 | } 142 | 143 | token = result.SecurityToken; 144 | identity = new ClaimsIdentity(result.ClaimsIdentity.Claims, "JWT"); 145 | 146 | if (validateExpiration && token.ValidTo == DateTime.MinValue) 147 | return ManifestStatus.Invalid; 148 | 149 | // The sponsorable manifest does not have an expiration time. 150 | if (validateExpiration && token.ValidTo < DateTimeOffset.UtcNow) 151 | return ManifestStatus.Expired; 152 | 153 | return ManifestStatus.Valid; 154 | } 155 | 156 | class JwtRolesPrincipal(ClaimsIdentity identity) : ClaimsPrincipal([identity]) 157 | { 158 | public override bool IsInRole(string role) => HasClaim("roles", role) || base.IsInRole(role); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink/SponsorStatus.cs: -------------------------------------------------------------------------------- 1 | // 2 | namespace Devlooped.Sponsors; 3 | 4 | public static class SponsorStatusExtensions 5 | { 6 | /// 7 | /// Whether represents a sponsor (directly or indirectly). 8 | /// 9 | public static bool IsSponsor(this SponsorStatus status) 10 | => status == SponsorStatus.User || 11 | status == SponsorStatus.Team || 12 | status == SponsorStatus.Contributor || 13 | status == SponsorStatus.Organization; 14 | } 15 | 16 | /// 17 | /// The determined sponsoring status. 18 | /// 19 | public enum SponsorStatus 20 | { 21 | /// 22 | /// Sponsorship status is unknown. 23 | /// 24 | Unknown, 25 | /// 26 | /// Sponsorship status is unknown, but within the grace period. 27 | /// 28 | Grace, 29 | /// 30 | /// The sponsors manifest is expired but within the grace period. 31 | /// 32 | Expiring, 33 | /// 34 | /// The sponsors manifest is expired and outside the grace period. 35 | /// 36 | Expired, 37 | /// 38 | /// The user is personally sponsoring. 39 | /// 40 | User, 41 | /// 42 | /// The user is a team member. 43 | /// 44 | Team, 45 | /// 46 | /// The user is a contributor. 47 | /// 48 | Contributor, 49 | /// 50 | /// The user is a member of a contributing organization. 51 | /// 52 | Organization, 53 | /// 54 | /// The user is a OSS author. 55 | /// 56 | OpenSource, 57 | } 58 | -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink/SponsorableLib.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $([System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory)sponsorable.md)) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | $(WarningsNotAsErrors);LIB001;LIB002;LIB003;LIB004;LIB005 16 | 17 | $(BaseIntermediateOutputPath)autosync.stamp 18 | 19 | $(HOME) 20 | $(USERPROFILE) 21 | 22 | true 23 | $([System.IO.Path]::GetFullPath('$(UserProfileHome)/.sponsorlink')) 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 48 | 49 | 50 | 51 | 52 | %(GitRoot.FullPath) 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink/Tracing.cs: -------------------------------------------------------------------------------- 1 | // 2 | #nullable enable 3 | using System; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Runtime.CompilerServices; 7 | using System.Text; 8 | 9 | namespace Devlooped.Sponsors; 10 | 11 | static class Tracing 12 | { 13 | public static void Trace([CallerMemberName] string? message = null, [CallerFilePath] string? filePath = null, [CallerLineNumber] int lineNumber = 0) 14 | { 15 | var trace = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("SPONSORLINK_TRACE")); 16 | #if DEBUG 17 | trace = true; 18 | #endif 19 | 20 | if (!trace) 21 | return; 22 | 23 | var line = new StringBuilder() 24 | .Append($"[{DateTime.Now:O}]") 25 | .Append($"[{Process.GetCurrentProcess().ProcessName}:{Process.GetCurrentProcess().Id}]") 26 | .Append($" {message} ") 27 | .AppendLine($" -> {filePath}({lineNumber})") 28 | .ToString(); 29 | 30 | var dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".sponsorlink"); 31 | Directory.CreateDirectory(dir); 32 | 33 | var tries = 0; 34 | // Best-effort only 35 | while (tries < 10) 36 | { 37 | try 38 | { 39 | File.AppendAllText(Path.Combine(dir, "trace.log"), line); 40 | Debugger.Log(0, "SponsorLink", line); 41 | return; 42 | } 43 | catch (IOException) 44 | { 45 | tries++; 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink/buildTransitive/Devlooped.Sponsors.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $([System.DateTime]::Now.ToString("yyyy-MM-yy")) 6 | 7 | $(BaseIntermediateOutputPath)autosync-$(Today).stamp 8 | 9 | $(BaseIntermediateOutputPath)autosync.stamp 10 | 11 | $(HOME) 12 | $(USERPROFILE) 13 | 14 | $([System.IO.Path]::GetFullPath('$(UserProfileHome)/.sponsorlink')) 15 | 16 | $([System.IO.Path]::Combine('$(SponsorLinkHome)', '.netconfig')) 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | SL_CollectDependencies;SL_CollectSponsorableAnalyzer 36 | $(SLDependsOn);SL_CheckAutoSync;SL_ReadAutoSyncEnabled;SL_SyncSponsors 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 53 | $([MSBuild]::ValueOrDefault('%(_RestoreGraphEntry.Id)', '').Replace('.', '_')) 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | <_FundingPackageId>%(FundingPackageId.Identity) 65 | 66 | 67 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 90 | 91 | 92 | %(SLConfigAutoSync.Identity) 93 | true 94 | false 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | $([System.IO.File]::ReadAllText($(AutoSyncStampFile)).Trim()) 103 | 104 | 105 | 106 | 107 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink/sponsorable.md: -------------------------------------------------------------------------------- 1 | # Why Sponsor 2 | 3 | Well, why not? It's super cheap :) 4 | 5 | This could even be partially auto-generated from FUNDING.yml and what-not. -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLinkAnalyzer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.10.34928.147 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzer", "Analyzer\Analyzer.csproj", "{584984D6-926B-423D-9416-519613423BAE}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Library", "Library\Library.csproj", "{598CD398-A172-492C-8367-827D43276029}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{EA02494C-6ED4-47A0-8D43-20F50BE8554F}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SponsorLink", "SponsorLink\SponsorLink.csproj", "{B91C7E99-3D2E-4FDF-B017-9123E810197F}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {584984D6-926B-423D-9416-519613423BAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {584984D6-926B-423D-9416-519613423BAE}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {584984D6-926B-423D-9416-519613423BAE}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {584984D6-926B-423D-9416-519613423BAE}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {598CD398-A172-492C-8367-827D43276029}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {598CD398-A172-492C-8367-827D43276029}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {598CD398-A172-492C-8367-827D43276029}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {598CD398-A172-492C-8367-827D43276029}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {EA02494C-6ED4-47A0-8D43-20F50BE8554F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {EA02494C-6ED4-47A0-8D43-20F50BE8554F}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {EA02494C-6ED4-47A0-8D43-20F50BE8554F}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {EA02494C-6ED4-47A0-8D43-20F50BE8554F}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {B91C7E99-3D2E-4FDF-B017-9123E810197F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {B91C7E99-3D2E-4FDF-B017-9123E810197F}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {B91C7E99-3D2E-4FDF-B017-9123E810197F}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {B91C7E99-3D2E-4FDF-B017-9123E810197F}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {1DDA0EFF-BEF6-49BB-8AA8-D71FE1CD3E6F} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /src/SponsorLink/Tests/.netconfig: -------------------------------------------------------------------------------- 1 | [config] 2 | root = true 3 | [file "SponsorableManifest.cs"] 4 | url = https://github.com/devlooped/SponsorLink/blob/main/src/Core/SponsorableManifest.cs 5 | sha = 5a4cad3a084f53afe34a6b75e4f3a084a0f1bf9e 6 | etag = 9a07c856d06e0cde629fce3ec014f64f9adfd5ae5805a35acf623eba0ee045c1 7 | weak 8 | [file "JsonOptions.cs"] 9 | url = https://github.com/devlooped/SponsorLink/blob/main/src/Core/JsonOptions.cs 10 | sha = 80ea1bfe47049ef6c6ed4f424dcf7febb729cbba 11 | etag = 17799725ad9b24eb5998365962c30b9a487bddadca37c616e35b76b8c9eb161a 12 | weak 13 | [file "Extensions.cs"] 14 | url = https://github.com/devlooped/SponsorLink/blob/main/src/Core/Extensions.cs 15 | sha = c455f6fa1a4d404181d076d7f3362345c8ed7df2 16 | etag = 9e51b7e6540fae140490a5283b1e67ce071bd18a267bc2ae0b35c7248261aed1 17 | weak -------------------------------------------------------------------------------- /src/SponsorLink/Tests/Attributes.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Xunit; 3 | 4 | public class SecretsFactAttribute : FactAttribute 5 | { 6 | public SecretsFactAttribute(params string[] secrets) 7 | { 8 | var configuration = new ConfigurationBuilder() 9 | .AddUserSecrets() 10 | .Build(); 11 | 12 | var missing = new HashSet(); 13 | 14 | foreach (var secret in secrets) 15 | { 16 | if (string.IsNullOrEmpty(configuration[secret])) 17 | missing.Add(secret); 18 | } 19 | 20 | if (missing.Count > 0) 21 | Skip = "Missing user secrets: " + string.Join(',', missing); 22 | } 23 | } 24 | 25 | public class LocalFactAttribute : SecretsFactAttribute 26 | { 27 | public LocalFactAttribute(params string[] secrets) : base(secrets) 28 | { 29 | if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"))) 30 | Skip = "Non-CI test"; 31 | } 32 | } 33 | 34 | public class CIFactAttribute : FactAttribute 35 | { 36 | public CIFactAttribute() 37 | { 38 | if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"))) 39 | Skip = "CI-only test"; 40 | } 41 | } 42 | 43 | public class LocalTheoryAttribute : TheoryAttribute 44 | { 45 | public LocalTheoryAttribute() 46 | { 47 | if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"))) 48 | Skip = "Non-CI test"; 49 | } 50 | } 51 | 52 | public class CITheoryAttribute : TheoryAttribute 53 | { 54 | public CITheoryAttribute() 55 | { 56 | if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"))) 57 | Skip = "CI-only test"; 58 | } 59 | } -------------------------------------------------------------------------------- /src/SponsorLink/Tests/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Runtime.CompilerServices; 3 | using System.Security.Cryptography; 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.IdentityModel.Tokens; 6 | 7 | namespace Devlooped.Sponsors; 8 | 9 | static class Extensions 10 | { 11 | public static HashCode Add(this HashCode hash, params object[] items) 12 | { 13 | foreach (var item in items) 14 | hash.Add(item); 15 | 16 | return hash; 17 | } 18 | 19 | 20 | public static HashCode AddRange(this HashCode hash, IEnumerable items) 21 | { 22 | foreach (var item in items) 23 | hash.Add(item); 24 | 25 | return hash; 26 | } 27 | 28 | public static bool ThumbprintEquals(this SecurityKey key, RSA rsa) => key.ThumbprintEquals(new RsaSecurityKey(rsa)); 29 | 30 | public static bool ThumbprintEquals(this RSA rsa, SecurityKey key) => key.ThumbprintEquals(rsa); 31 | 32 | public static bool ThumbprintEquals(this SecurityKey first, SecurityKey second) 33 | { 34 | var expectedKey = JsonWebKeyConverter.ConvertFromSecurityKey(second); 35 | var actualKey = JsonWebKeyConverter.ConvertFromSecurityKey(first); 36 | return expectedKey.ComputeJwkThumbprint().AsSpan().SequenceEqual(actualKey.ComputeJwkThumbprint()); 37 | } 38 | 39 | public static Array Cast(this Array array, Type elementType) 40 | { 41 | //Convert the object list to the destination array type. 42 | var result = Array.CreateInstance(elementType, array.Length); 43 | Array.Copy(array, result, array.Length); 44 | return result; 45 | } 46 | 47 | public static void Assert(this ILogger logger, [DoesNotReturnIf(false)] bool condition, [CallerArgumentExpression(nameof(condition))] string? message = default, params object?[] args) 48 | { 49 | if (!condition) 50 | { 51 | //Debug.Assert(condition, message); 52 | logger.LogError(message, args); 53 | throw new InvalidOperationException(message); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/SponsorLink/Tests/JsonOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | using System.Text.Json.Serialization.Metadata; 5 | using Microsoft.IdentityModel.Tokens; 6 | 7 | namespace Devlooped.Sponsors; 8 | 9 | static partial class JsonOptions 10 | { 11 | public static JsonSerializerOptions Default { get; } = 12 | #if NET6_0_OR_GREATER 13 | new(JsonSerializerDefaults.Web) 14 | #else 15 | new() 16 | #endif 17 | { 18 | AllowTrailingCommas = true, 19 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 20 | ReadCommentHandling = JsonCommentHandling.Skip, 21 | #if NET6_0_OR_GREATER 22 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault | JsonIgnoreCondition.WhenWritingNull, 23 | #endif 24 | WriteIndented = true, 25 | Converters = 26 | { 27 | new JsonStringEnumConverter(allowIntegerValues: false), 28 | #if NET6_0_OR_GREATER 29 | new DateOnlyJsonConverter() 30 | #endif 31 | } 32 | }; 33 | 34 | public static JsonSerializerOptions JsonWebKey { get; } = new(JsonSerializerOptions.Default) 35 | { 36 | WriteIndented = true, 37 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault | JsonIgnoreCondition.WhenWritingNull, 38 | TypeInfoResolver = new DefaultJsonTypeInfoResolver 39 | { 40 | Modifiers = 41 | { 42 | info => 43 | { 44 | if (info.Type != typeof(JsonWebKey)) 45 | return; 46 | 47 | foreach (var prop in info.Properties) 48 | { 49 | // Don't serialize empty lists, makes for more concise JWKs 50 | prop.ShouldSerialize = (obj, value) => 51 | value is not null && 52 | (value is not IList list || list.Count > 0); 53 | } 54 | } 55 | } 56 | } 57 | }; 58 | 59 | 60 | #if NET6_0_OR_GREATER 61 | public class DateOnlyJsonConverter : JsonConverter 62 | { 63 | public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 64 | => DateOnly.Parse(reader.GetString()?[..10] ?? "", CultureInfo.InvariantCulture); 65 | 66 | public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) 67 | => writer.WriteStringValue(value.ToString("O", CultureInfo.InvariantCulture)); 68 | } 69 | #endif 70 | } 71 | -------------------------------------------------------------------------------- /src/SponsorLink/Tests/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | text/microsoft-resx 91 | 92 | 93 | 1.3 94 | 95 | 96 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 97 | 98 | 99 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 100 | 101 | -------------------------------------------------------------------------------- /src/SponsorLink/Tests/Sample.cs: -------------------------------------------------------------------------------- 1 | extern alias Analyzer; 2 | using System; 3 | using System.Globalization; 4 | using System.Runtime.CompilerServices; 5 | using System.Security.Cryptography; 6 | using Analyzer::Devlooped.Sponsors; 7 | using Microsoft.CodeAnalysis; 8 | using Xunit; 9 | using Xunit.Abstractions; 10 | 11 | namespace Tests; 12 | 13 | public class Sample(ITestOutputHelper output) 14 | { 15 | [Theory] 16 | [InlineData("es-AR", SponsorStatus.Unknown)] 17 | [InlineData("es-AR", SponsorStatus.Expiring)] 18 | [InlineData("es-AR", SponsorStatus.Expired)] 19 | [InlineData("es-AR", SponsorStatus.User)] 20 | [InlineData("es-AR", SponsorStatus.Contributor)] 21 | [InlineData("es", SponsorStatus.Unknown)] 22 | [InlineData("es", SponsorStatus.Expiring)] 23 | [InlineData("es", SponsorStatus.Expired)] 24 | [InlineData("es", SponsorStatus.User)] 25 | [InlineData("es", SponsorStatus.Contributor)] 26 | [InlineData("en", SponsorStatus.Unknown)] 27 | [InlineData("en", SponsorStatus.Expiring)] 28 | [InlineData("en", SponsorStatus.Expired)] 29 | [InlineData("en", SponsorStatus.User)] 30 | [InlineData("en", SponsorStatus.Contributor)] 31 | [InlineData("", SponsorStatus.Unknown)] 32 | [InlineData("", SponsorStatus.Expiring)] 33 | [InlineData("", SponsorStatus.Expired)] 34 | [InlineData("", SponsorStatus.User)] 35 | [InlineData("", SponsorStatus.Contributor)] 36 | public void Test(string culture, SponsorStatus kind) 37 | { 38 | Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture = 39 | culture == "" ? CultureInfo.InvariantCulture : CultureInfo.GetCultureInfo(culture); 40 | 41 | var diag = GetDescriptor(["foo"], "bar", "FB", kind); 42 | 43 | output.WriteLine(diag.Title.ToString()); 44 | output.WriteLine(diag.MessageFormat.ToString()); 45 | output.WriteLine(diag.Description.ToString()); 46 | } 47 | 48 | [Fact] 49 | public void RenderSponsorables() 50 | { 51 | Assert.NotEmpty(SponsorLink.Sponsorables); 52 | 53 | foreach (var pair in SponsorLink.Sponsorables) 54 | { 55 | output.WriteLine($"{pair.Key} = {pair.Value}"); 56 | // Read the JWK 57 | var jsonWebKey = Microsoft.IdentityModel.Tokens.JsonWebKey.Create(pair.Value); 58 | 59 | Assert.NotNull(jsonWebKey); 60 | 61 | using var key = RSA.Create(new RSAParameters 62 | { 63 | Modulus = Microsoft.IdentityModel.Tokens.Base64UrlEncoder.DecodeBytes(jsonWebKey.N), 64 | Exponent = Microsoft.IdentityModel.Tokens.Base64UrlEncoder.DecodeBytes(jsonWebKey.E), 65 | }); 66 | } 67 | } 68 | 69 | DiagnosticDescriptor GetDescriptor(string[] sponsorable, string product, string prefix, SponsorStatus status) => status switch 70 | { 71 | SponsorStatus.Unknown => DiagnosticsManager.CreateUnknown(sponsorable, product, prefix), 72 | SponsorStatus.Expiring => DiagnosticsManager.CreateExpiring(sponsorable, prefix), 73 | SponsorStatus.Expired => DiagnosticsManager.CreateExpired(sponsorable, prefix), 74 | SponsorStatus.User => DiagnosticsManager.CreateSponsor(sponsorable, prefix), 75 | SponsorStatus.Contributor => DiagnosticsManager.CreateContributor(sponsorable, prefix), 76 | _ => throw new NotImplementedException(), 77 | }; 78 | } -------------------------------------------------------------------------------- /src/SponsorLink/Tests/SponsorManifestTests.cs: -------------------------------------------------------------------------------- 1 | extern alias Analyzer; 2 | using System.Security.Cryptography; 3 | using System.Text.Json; 4 | using Analyzer::Devlooped.Sponsors; 5 | using Devlooped.Sponsors; 6 | using Microsoft.IdentityModel.Tokens; 7 | using Xunit; 8 | 9 | namespace Devlooped.Tests; 10 | 11 | public class SponsorManifestTests 12 | { 13 | // We need to convert to jwk string since the analyzer project has merged the JWT assembly and types. 14 | public static string ToJwk(SecurityKey key) 15 | => JsonSerializer.Serialize( 16 | JsonWebKeyConverter.ConvertFromSecurityKey(key), 17 | JsonOptions.JsonWebKey); 18 | 19 | [Fact] 20 | public void ValidateSponsorable() 21 | { 22 | var sponsorable = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234"); 23 | var jwt = sponsorable.ToJwt(); 24 | var jwk = ToJwk(sponsorable.SecurityKey); 25 | 26 | // NOTE: sponsorable manifest doesn't have expiration date. 27 | var manifest = SponsorLink.ParseManifest(jwt, jwk, false); 28 | 29 | Assert.True(manifest.IsValid); 30 | Assert.Equal(ManifestStatus.Valid, manifest.Status); 31 | } 32 | 33 | [Fact] 34 | public void ValidateWrongKey() 35 | { 36 | var sponsorable = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234"); 37 | var jwt = sponsorable.ToJwt(); 38 | var jwk = ToJwk(new RsaSecurityKey(RSA.Create())); 39 | 40 | var manifest = SponsorLink.ParseManifest(jwt, jwk, false); 41 | 42 | Assert.Equal(ManifestStatus.Invalid, manifest.Status); 43 | 44 | // We should still be a able to read the data, knowing it may have been tampered with. 45 | Assert.NotNull(manifest.Principal); 46 | Assert.NotNull(manifest.SecurityToken); 47 | } 48 | 49 | [Fact] 50 | public void ValidateExpiredSponsor() 51 | { 52 | var sponsorable = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234"); 53 | var jwk = ToJwk(sponsorable.SecurityKey); 54 | var sponsor = sponsorable.Sign([], expiration: TimeSpan.Zero); 55 | 56 | // Will be expired after this. 57 | Thread.Sleep(1000); 58 | 59 | var manifest = SponsorLink.ParseManifest(sponsor, jwk, true); 60 | 61 | Assert.Equal(ManifestStatus.Expired, manifest.Status); 62 | 63 | // We should still be a able to read the data, even if expired (but not tampered with). 64 | Assert.NotNull(manifest.Principal); 65 | Assert.NotNull(manifest.SecurityToken); 66 | } 67 | 68 | [Fact] 69 | public void ValidateUnknownFormat() 70 | { 71 | var sponsorable = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234"); 72 | var jwk = ToJwk(sponsorable.SecurityKey); 73 | 74 | var manifest = SponsorLink.ParseManifest("asdfasdf", jwk, false); 75 | 76 | Assert.Equal(ManifestStatus.Unknown, manifest.Status); 77 | 78 | // Nothing could be read at all. 79 | Assert.False(manifest.IsValid); 80 | Assert.NotNull(manifest.Principal); 81 | Assert.Null(manifest.Principal.Identity); 82 | Assert.Null(manifest.SecurityToken); 83 | } 84 | 85 | [Fact] 86 | public void TryRead() 87 | { 88 | var fooSponsorable = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/foo")], "ASDF1234"); 89 | var barSponsorable = SponsorableManifest.Create(new Uri("https://bar.com"), [new Uri("https://github.com/sponsors/bar")], "GHJK5678"); 90 | 91 | // Org sponsor and member of team 92 | var fooSponsor = fooSponsorable.Sign([new("sub", "kzu"), new("email", "me@foo.com"), new("roles", "org"), new("roles", "team")], expiration: TimeSpan.FromDays(30)); 93 | // Org + personal sponsor 94 | var barSponsor = barSponsorable.Sign([new("sub", "kzu"), new("email", "me@bar.com"), new("roles", "org"), new("roles", "user")], expiration: TimeSpan.FromDays(30)); 95 | 96 | Assert.True(SponsorLink.TryRead(out var principal, [(fooSponsor, ToJwk(fooSponsorable.SecurityKey)), (barSponsor, ToJwk(barSponsorable.SecurityKey))])); 97 | 98 | // Can check role across both JWTs 99 | Assert.True(principal.IsInRole("org")); 100 | Assert.True(principal.IsInRole("team")); 101 | Assert.True(principal.IsInRole("user")); 102 | 103 | Assert.True(principal.HasClaim("sub", "kzu")); 104 | Assert.True(principal.HasClaim("email", "me@foo.com")); 105 | Assert.True(principal.HasClaim("email", "me@bar.com")); 106 | } 107 | 108 | [LocalFact] 109 | public void ValidateCachedManifest() 110 | { 111 | var path = Environment.ExpandEnvironmentVariables("%userprofile%\\.sponsorlink\\github\\devlooped.jwt"); 112 | if (!File.Exists(path)) 113 | return; 114 | 115 | var jwt = File.ReadAllText(path); 116 | 117 | var manifest = SponsorLink.ParseManifest(jwt, 118 | """ 119 | { 120 | "e": "AQAB", 121 | "kty": "RSA", 122 | "n": "5inhv8QymaDBOihNi1eY-6-hcIB5qSONFZxbxxXAyOtxAdjFCPM-94gIZqM9CDrX3pyg1lTJfml_a_FZSU9dB1ii5mSX_mNHBFXn1_l_gi1ErdbkIF5YbW6oxWFxf3G5mwVXwnPfxHTyQdmWQ3YJR-A3EB4kaFwLqA6Ha5lb2ObGpMTQJNakD4oTAGDhqHMGhu6PupGq5ie4qZcQ7N8ANw8xH7nicTkbqEhQABHWOTmLBWq5f5F6RYGF8P7cl0IWl_w4YcIZkGm2vX2fi26F9F60cU1v13GZEVDTXpJ9kzvYeM9sYk6fWaoyY2jhE51qbv0B0u6hScZiLREtm3n7ClJbIGXhkUppFS2JlNaX3rgQ6t-4LK8gUTyLt3zDs2H8OZyCwlCpfmGmdsUMkm1xX6t2r-95U3zywynxoWZfjBCJf41leM9OMKYwNWZ6LQMyo83HWw1PBIrX4ZLClFwqBcSYsXDyT8_ZLd1cdYmPfmtllIXxZhLClwT5qbCWv73V" 123 | } 124 | """ 125 | , false); 126 | 127 | Assert.Equal(ManifestStatus.Valid, manifest.Status); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/SponsorLink/Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | true 6 | CS8981;$(NoWarn) 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | %(GitRoot.FullPath) 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | true 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/SponsorLink/Tests/keys/kzu.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devlooped/WebSocketChannel/5b9d995dc1e7503d0e9d56f23cb22e9aa0c5bbfa/src/SponsorLink/Tests/keys/kzu.key -------------------------------------------------------------------------------- /src/SponsorLink/Tests/keys/kzu.key.jwk: -------------------------------------------------------------------------------- 1 | { 2 | "d": "OmDrKyd0ap7Az2V09C8E6aASK0nnXHGUdTIymmU1tGoxaWpkZ4gLGaYDp4L5fKc-AaqqD3PjvfJvEWfXLqJtEWfUl4gahWrgUkmuzPyAVFFioIzeGIvTGELsR6lTRke0IB2kvfvS7hRgX8Py8ohCAGHiidmoec2SyKkEg0aPdxrIKV8hx5ybC_D_4zRKn0GuVwATIeVZzPpTcyJX_sn4NHDOqut0Xg02iHhMKpF850BSoC97xGMlcSjLocFSTwI63msz6jWQ-6LRVXsfRr2mqakAvsPpqEQ3Ytk9Ud9xW0ctuAWyo6UXev5w2XEL8cSXm33-57fi3ekC_jGCqW0KfAU4Cr2UbuTC0cv8Vv0F4Xm5FizolmuSBFOvf55-eqsjmpwf9hftYAiIlFF-49-P0DpeJejSeoL06BE3e3_IVu3g3HNnSWVUOLJ5Uk5FQ-ieHhf-r2Tq5qZ8_-losHekQbCxCMY2isc-r6V6BMnVL_9kWPxpXwhjKrYxNFZEXUJ1", 3 | "dp": "HjCs_QF1Hn1SGS2OqZzYhGhNk4PTw9Hs97E7g3pb4liY0uECYOYp1RNoMyzvNBZVwlxhpeTTS299yPoXeYmseXfLtcjfIVi6mSWS_u27Hd0zfSdaPDOXyyK-mZfIV7Q76RTost0QY3LA0ciJbj3gJqpl38dhuNQ8h9Yqt-TFyb3CUaM3A_JUNKOTce8qnkLrasytPEuSroOBT8bgCWJIjw_mXWMGcoqRFWHw9Nyp9mIyvtPjUQ9ig3bGSP_-3LZf", 4 | "dq": "IP6EsAZ_6psFdlQrvnugYFs91fEP5QfBzNHmbfmsPRVX4QM5B3L6klQyJsLqvPfF1Xu17ZffLFkNBKuiphIcLPo0yZTJG9Y7S8gLuPAmrH-ndfxG-bQ8Yt0ZB1pA77ILIS8bUTKrMqAWS-VcaxcSCIyhSusLEWYYDi3PEzB375OUw4aXIk3ob8bePG7UqFSL6qmDPgkGLTxkY9m5dEiOshHygtVY-H_jjOIawliEPgmgAr2M-zlXiphovDyAT0PV", 5 | "e": "AQAB", 6 | "kty": "RSA", 7 | "n": "yP71VgOgHDtGbxdyN31mIFFITmGYEk2cwepKbyqKTbTYXF1OXaMoP5n3mfwqwzUQmEAsrclAigPcK4GIy5WWlc5YujIxKauJjsKe0FBxMnFp9o1UcBUHfgDJjaAKieQxb44717b1MwCcflEGnCGTXkntdr45y9Gi1D9-oBw5zIVZekgMP55XxmKvkJd1k-bYWSv-QFG2JJwRIGwr29Jr62juCsLB7Tg83ZGKCa22Y_7lQcezxRRD5OrGWhf3gTYArbrEzbYy653zbHfbOCJeVBe_bXDkR74yG3mmq_Ne0qhNk6wXuX-NrKEvdPxRSRBF7C465fcVY9PM6eTqEPQwKDiarHpU1NTwUetzb-YKry-h678RJWMhC7I9lzzWVobbC0YVKG7XpeVqBB4u7Q6cGo5Xkf19VldkIxQMu9sFeuHGDSoiCLqmRmwNn9GsMV77oZWr-OPrxEdZzL9BcI4fMJMz7YdiIu-qbIp_vqatbalfNasumf8RgtPOkR2vgc59", 8 | "p": "6JTf8Qb0iHRL6MIIs7MlkEKBNpnAu_Nie4HTqhxy2wfE4cBr6QZ98iJniXffDIjq_GxVpw9K-Bv2gTcNrlzOiBaLf3X2Itfice_Qd-luhNbnXVfiA5sg6dZ2wbBuue5ann5iJ_TIbxO4CLUiqQp0PCReUPzTQhzesHxM2-dBC9AYDl7P6p1FF53Hh_Knx9UywhoPvNtoCJy35-5rj0ghgPYz289dbOBccZnvabRueOr_wpHGMKaznqiDMrcFSZ07", 9 | "q": "3TvrN8R9imw6E6JkVQ4PtveE0vkvkSWHUpn9KwKFIJJiwL_HSS4z_8IYR1_0Q1OgK5-z-QcXhq9P7jTjz02I2uwWhP3RZQf99RZACfMaeIs8O2V-I89WdlJYOerzAelW4nYw7zyeVoT5c5osicGWfSmWslLRjA1yx7x1KA_MCU_KIEBlpe1RgEUYPET3OtvPKFIVQYoJfQC5PFlmrC-kgHZMSpdHjWgWi5gPn0fIBCKFsXcPrt2n_lKKGc4lFOen", 10 | "qi": "m-tgdFqO1Ax3C00oe7kdkYLHMD56wkGARdqPCqS5IGhFVKCOA8U6O_s5bSL4r0TzPE0KrJ4A5QJEwjbH4bXssPaaAlv1ZdWjn8YMQCYFolg_pgUWYYI5vNxG1gIsLGXPTfE8a6SObkJ2Q9VC5ZZp14r4lPvJhwFICIGSRBKcvS-gO_gqB3LKuG9TQBi-CE4DHDLJwsCbEBR8Ber45oTqvG7hphpOhBHsFZ8_6f3Reg_sK1BCz9HFCx8hhi8rBfUp" 11 | } -------------------------------------------------------------------------------- /src/SponsorLink/Tests/keys/kzu.key.txt: -------------------------------------------------------------------------------- 1 | MIIG4wIBAAKCAYEAyP71VgOgHDtGbxdyN31mIFFITmGYEk2cwepKbyqKTbTYXF1OXaMoP5n3mfwqwzUQmEAsrclAigPcK4GIy5WWlc5YujIxKauJjsKe0FBxMnFp9o1UcBUHfgDJjaAKieQxb44717b1MwCcflEGnCGTXkntdr45y9Gi1D9+oBw5zIVZekgMP55XxmKvkJd1k+bYWSv+QFG2JJwRIGwr29Jr62juCsLB7Tg83ZGKCa22Y/7lQcezxRRD5OrGWhf3gTYArbrEzbYy653zbHfbOCJeVBe/bXDkR74yG3mmq/Ne0qhNk6wXuX+NrKEvdPxRSRBF7C465fcVY9PM6eTqEPQwKDiarHpU1NTwUetzb+YKry+h678RJWMhC7I9lzzWVobbC0YVKG7XpeVqBB4u7Q6cGo5Xkf19VldkIxQMu9sFeuHGDSoiCLqmRmwNn9GsMV77oZWr+OPrxEdZzL9BcI4fMJMz7YdiIu+qbIp/vqatbalfNasumf8RgtPOkR2vgc59AgMBAAECggGAOmDrKyd0ap7Az2V09C8E6aASK0nnXHGUdTIymmU1tGoxaWpkZ4gLGaYDp4L5fKc+AaqqD3PjvfJvEWfXLqJtEWfUl4gahWrgUkmuzPyAVFFioIzeGIvTGELsR6lTRke0IB2kvfvS7hRgX8Py8ohCAGHiidmoec2SyKkEg0aPdxrIKV8hx5ybC/D/4zRKn0GuVwATIeVZzPpTcyJX/sn4NHDOqut0Xg02iHhMKpF850BSoC97xGMlcSjLocFSTwI63msz6jWQ+6LRVXsfRr2mqakAvsPpqEQ3Ytk9Ud9xW0ctuAWyo6UXev5w2XEL8cSXm33+57fi3ekC/jGCqW0KfAU4Cr2UbuTC0cv8Vv0F4Xm5FizolmuSBFOvf55+eqsjmpwf9hftYAiIlFF+49+P0DpeJejSeoL06BE3e3/IVu3g3HNnSWVUOLJ5Uk5FQ+ieHhf+r2Tq5qZ8/+losHekQbCxCMY2isc+r6V6BMnVL/9kWPxpXwhjKrYxNFZEXUJ1AoHBAOiU3/EG9Ih0S+jCCLOzJZBCgTaZwLvzYnuB06occtsHxOHAa+kGffIiZ4l33wyI6vxsVacPSvgb9oE3Da5czogWi3919iLX4nHv0HfpboTW511X4gObIOnWdsGwbrnuWp5+Yif0yG8TuAi1IqkKdDwkXlD800Ic3rB8TNvnQQvQGA5ez+qdRRedx4fyp8fVMsIaD7zbaAict+fua49IIYD2M9vPXWzgXHGZ72m0bnjq/8KRxjCms56ogzK3BUmdOwKBwQDdO+s3xH2KbDoTomRVDg+294TS+S+RJYdSmf0rAoUgkmLAv8dJLjP/whhHX/RDU6Arn7P5BxeGr0/uNOPPTYja7BaE/dFlB/31FkAJ8xp4izw7ZX4jz1Z2Ulg56vMB6VbidjDvPJ5WhPlzmiyJwZZ9KZayUtGMDXLHvHUoD8wJT8ogQGWl7VGARRg8RPc6288oUhVBigl9ALk8WWasL6SAdkxKl0eNaBaLmA+fR8gEIoWxdw+u3af+UooZziUU56cCgcAeMKz9AXUefVIZLY6pnNiEaE2Tg9PD0ez3sTuDelviWJjS4QJg5inVE2gzLO80FlXCXGGl5NNLb33I+hd5iax5d8u1yN8hWLqZJZL+7bsd3TN9J1o8M5fLIr6Zl8hXtDvpFOiy3RBjcsDRyIluPeAmqmXfx2G41DyH1iq35MXJvcJRozcD8lQ0o5Nx7yqeQutqzK08S5Kug4FPxuAJYkiPD+ZdYwZyipEVYfD03Kn2YjK+0+NRD2KDdsZI//7ctl8CgcAg/oSwBn/qmwV2VCu+e6BgWz3V8Q/lB8HM0eZt+aw9FVfhAzkHcvqSVDImwuq898XVe7Xtl98sWQ0Eq6KmEhws+jTJlMkb1jtLyAu48Casf6d1/Eb5tDxi3RkHWkDvsgshLxtRMqsyoBZL5VxrFxIIjKFK6wsRZhgOLc8TMHfvk5TDhpciTehvxt48btSoVIvqqYM+CQYtPGRj2bl0SI6yEfKC1Vj4f+OM4hrCWIQ+CaACvYz7OVeKmGi8PIBPQ9UCgcEAm+tgdFqO1Ax3C00oe7kdkYLHMD56wkGARdqPCqS5IGhFVKCOA8U6O/s5bSL4r0TzPE0KrJ4A5QJEwjbH4bXssPaaAlv1ZdWjn8YMQCYFolg/pgUWYYI5vNxG1gIsLGXPTfE8a6SObkJ2Q9VC5ZZp14r4lPvJhwFICIGSRBKcvS+gO/gqB3LKuG9TQBi+CE4DHDLJwsCbEBR8Ber45oTqvG7hphpOhBHsFZ8/6f3Reg/sK1BCz9HFCx8hhi8rBfUp -------------------------------------------------------------------------------- /src/SponsorLink/Tests/keys/kzu.pub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devlooped/WebSocketChannel/5b9d995dc1e7503d0e9d56f23cb22e9aa0c5bbfa/src/SponsorLink/Tests/keys/kzu.pub -------------------------------------------------------------------------------- /src/SponsorLink/Tests/keys/kzu.pub.jwk: -------------------------------------------------------------------------------- 1 | { 2 | "e": "AQAB", 3 | "kty": "RSA", 4 | "n": "yP71VgOgHDtGbxdyN31mIFFITmGYEk2cwepKbyqKTbTYXF1OXaMoP5n3mfwqwzUQmEAsrclAigPcK4GIy5WWlc5YujIxKauJjsKe0FBxMnFp9o1UcBUHfgDJjaAKieQxb44717b1MwCcflEGnCGTXkntdr45y9Gi1D9-oBw5zIVZekgMP55XxmKvkJd1k-bYWSv-QFG2JJwRIGwr29Jr62juCsLB7Tg83ZGKCa22Y_7lQcezxRRD5OrGWhf3gTYArbrEzbYy653zbHfbOCJeVBe_bXDkR74yG3mmq_Ne0qhNk6wXuX-NrKEvdPxRSRBF7C465fcVY9PM6eTqEPQwKDiarHpU1NTwUetzb-YKry-h678RJWMhC7I9lzzWVobbC0YVKG7XpeVqBB4u7Q6cGo5Xkf19VldkIxQMu9sFeuHGDSoiCLqmRmwNn9GsMV77oZWr-OPrxEdZzL9BcI4fMJMz7YdiIu-qbIp_vqatbalfNasumf8RgtPOkR2vgc59" 5 | } -------------------------------------------------------------------------------- /src/SponsorLink/Tests/keys/kzu.pub.txt: -------------------------------------------------------------------------------- 1 | MIIBigKCAYEAyP71VgOgHDtGbxdyN31mIFFITmGYEk2cwepKbyqKTbTYXF1OXaMoP5n3mfwqwzUQmEAsrclAigPcK4GIy5WWlc5YujIxKauJjsKe0FBxMnFp9o1UcBUHfgDJjaAKieQxb44717b1MwCcflEGnCGTXkntdr45y9Gi1D9+oBw5zIVZekgMP55XxmKvkJd1k+bYWSv+QFG2JJwRIGwr29Jr62juCsLB7Tg83ZGKCa22Y/7lQcezxRRD5OrGWhf3gTYArbrEzbYy653zbHfbOCJeVBe/bXDkR74yG3mmq/Ne0qhNk6wXuX+NrKEvdPxRSRBF7C465fcVY9PM6eTqEPQwKDiarHpU1NTwUetzb+YKry+h678RJWMhC7I9lzzWVobbC0YVKG7XpeVqBB4u7Q6cGo5Xkf19VldkIxQMu9sFeuHGDSoiCLqmRmwNn9GsMV77oZWr+OPrxEdZzL9BcI4fMJMz7YdiIu+qbIp/vqatbalfNasumf8RgtPOkR2vgc59AgMBAAE= -------------------------------------------------------------------------------- /src/SponsorLink/Tests/keys/sponsorlink.jwt: -------------------------------------------------------------------------------- 1 | eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MTk4NjgyMzAsImlzcyI6Imh0dHBzOi8vc3BvbnNvcmxpbmsuZGV2bG9vcGVkLmNvbS8iLCJhdWQiOlsiaHR0cHM6Ly9naXRodWIuY29tL3Nwb25zb3JzL2t6dSIsImh0dHBzOi8vZ2l0aHViLmNvbS9zcG9uc29ycy9kZXZsb29wZWQiXSwiY2xpZW50X2lkIjoiYTgyMzUwZmIyYmFlNDA3YjMwMjEiLCJzdWJfandrIjp7ImUiOiJBUUFCIiwia3R5IjoiUlNBIiwibiI6InlQNzFWZ09nSER0R2J4ZHlOMzFtSUZGSVRtR1lFazJjd2VwS2J5cUtUYlRZWEYxT1hhTW9QNW4zbWZ3cXd6VVFtRUFzcmNsQWlnUGNLNEdJeTVXV2xjNVl1akl4S2F1SmpzS2UwRkJ4TW5GcDlvMVVjQlVIZmdESmphQUtpZVF4YjQ0NzE3YjFNd0NjZmxFR25DR1RYa250ZHI0NXk5R2kxRDktb0J3NXpJVlpla2dNUDU1WHhtS3ZrSmQxay1iWVdTdi1RRkcySkp3UklHd3IyOUpyNjJqdUNzTEI3VGc4M1pHS0NhMjJZXzdsUWNlenhSUkQ1T3JHV2hmM2dUWUFyYnJFemJZeTY1M3piSGZiT0NKZVZCZV9iWERrUjc0eUczbW1xX05lMHFoTms2d1h1WC1OcktFdmRQeFJTUkJGN0M0NjVmY1ZZOVBNNmVUcUVQUXdLRGlhckhwVTFOVHdVZXR6Yi1ZS3J5LWg2NzhSSldNaEM3STlsenpXVm9iYkMwWVZLRzdYcGVWcUJCNHU3UTZjR281WGtmMTlWbGRrSXhRTXU5c0ZldUhHRFNvaUNMcW1SbXdObjlHc01WNzdvWldyLU9QcnhFZFp6TDlCY0k0Zk1KTXo3WWRpSXUtcWJJcF92cWF0YmFsZk5hc3VtZjhSZ3RQT2tSMnZnYzU5In19.er4apYbEjHVKlQ_aMXoRhHYeR8N-3uIrCk3HX8UuZO7mb0CaS94-422EI3z5O9vRvckcGkNVoiSIX0ykZqUMHTZxBae-QZc1u_rhdBOChoaxWqpUiPXLZ5-yi7mcRwqg2DOUb2eHTNfRjwJ-0tjL1R1TqZw9d8Bgku1zw2ZTuJl_WsBRHKHTD_s5KyCP5yhSOUumrsf3nXYrc20fJ7ql0FsL0MP66utJk7TFYHGhQV3cfcXYqFEpv-k6tqB9k3Syc0UnepmQT0Y3dtcBzQzCOzfKQ8bdaAXVHjfp4VvXBluHmh9lP6TeZmpvlmQDFvyk0kp1diTbo9pqmX_llNDWNxBdvaSZGa7RZMG_dE2WJGtQNu0C_sbEZDPZsKncxdtm-j-6Y7GRqx7uxe4Py8tAZ7SxjiPgD64jf9KF2OT6f6drVtzohVzYCs6-vhcXzC2sQvd_gQ-SoFNTa1MEcMgGbL-fFWUC7-7bQV1DlSg2YFwrxEIwbM-gHpLZHyyJLvYD -------------------------------------------------------------------------------- /src/SponsorLink/jwk.ps1: -------------------------------------------------------------------------------- 1 | curl https://raw.githubusercontent.com/devlooped/.github/main/sponsorlink.jwt --silent | jq -R 'split(".") | .[1] | @base64d | fromjson' | jq '.sub_jwk' -------------------------------------------------------------------------------- /src/SponsorLink/readme.md: -------------------------------------------------------------------------------- 1 | # SponsorLink .NET Analyzer Sample 2 | 3 | This is one opinionated implementation of [SponsorLink](https://devlooped.com/SponsorLink) 4 | for .NET projects leveraging Roslyn analyzers. 5 | 6 | It is intended for use by [devlooped](https://github.com/devlooped) projects, but can be 7 | used as a template for other sponsorables as well. Supporting arbitrary sponsoring scenarios 8 | is out of scope though, since we just use GitHub sponsors for now. 9 | 10 | ## Usage 11 | 12 | A project can include all the necessary files by using the [dotnet-file](https://github.com/devlooped/dotnet-file) 13 | tool and sync all files to a folder, such as: 14 | 15 | ```shell 16 | dotnet file add https://github.com/devlooped/SponsorLink/tree/main/samples/dotnet/ src/SponsorLink/ 17 | ``` 18 | 19 | Including the analyzer and targets in a project involves two steps. 20 | 21 | 1. Create an analyzer project and add the following property: 22 | 23 | ```xml 24 | 25 | ... 26 | $(MSBuildThisFileDirectory)..\SponsorLink\SponsorLink.Analyzer.targets 27 | 28 | ``` 29 | 30 | 2. Add a `buildTransitive\[PackageId].targets` file with the following import: 31 | 32 | ```xml 33 | 34 | 35 | 36 | ``` 37 | 38 | 3. Set the package id(s) that will be checked for funding in the analyzer, such as: 39 | 40 | ```xml 41 | 42 | SponsorableLib;SponsorableLib.Core 43 | 44 | ``` 45 | 46 | The default analyzer will report a diagnostic for sponsorship status only 47 | if the project being compiled as a direct package reference to one of the 48 | specified package ids. 49 | 50 | This property defaults to `$(PackageId)` if present. Otherwise, it defaults 51 | to `$(FundingProduct)`, which in turn defaults to `$(Product)` if not provided. 52 | 53 | As long as NuGetizer is used, the right packaging will be done automatically. -------------------------------------------------------------------------------- /src/Tests/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:52411/", 7 | "sslPort": 44389 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Tests": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Tests/WebSocketServer.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Sockets; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.Extensions.Hosting; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Devlooped.Net; 8 | 9 | public record WebSocketServer(Uri Uri, Task Completion, CancellationTokenSource Cancellation) : IAsyncDisposable, IDisposable 10 | { 11 | static int serverPort = 10000; 12 | 13 | public static WebSocketServer Create(ITestOutputHelper? output = null) 14 | => Create(Echo, output); 15 | 16 | public static WebSocketServer Create(Func behavior, ITestOutputHelper? output = null) 17 | { 18 | var builder = WebApplication.CreateBuilder(new WebApplicationOptions 19 | { 20 | EnvironmentName = Environments.Development 21 | }); 22 | 23 | var port = Interlocked.Increment(ref serverPort); 24 | // test port availability by attempting to bind a listener to it, and increment 25 | // until we get a free one 26 | while (true) 27 | { 28 | try 29 | { 30 | using var listener = new TcpListener(IPAddress.Loopback, port); 31 | listener.Start(); 32 | listener.Stop(); 33 | break; 34 | } 35 | catch 36 | { 37 | port = Interlocked.Increment(ref serverPort); 38 | } 39 | } 40 | 41 | // Only turn on output loggig when running tests in the IDE, for easier troubleshooting. 42 | if (output != null && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("VSAPPIDNAME"))) 43 | builder.Logging.AddProvider(new LoggingProvider(output)); 44 | 45 | var app = builder.Build(); 46 | app.Urls.Add("http://localhost:" + port); 47 | 48 | app.UseWebSockets(); 49 | 50 | var cts = new CancellationTokenSource(); 51 | 52 | app.Use(async (context, next) => 53 | { 54 | if (!context.WebSockets.IsWebSocketRequest) 55 | { 56 | context.Response.StatusCode = (int)HttpStatusCode.BadRequest; 57 | await next(); 58 | } 59 | else 60 | { 61 | using var websocket = await context.WebSockets.AcceptWebSocketAsync( 62 | context.WebSockets.WebSocketRequestedProtocols.FirstOrDefault()); 63 | 64 | await behavior(websocket, cts.Token); 65 | //await Task.Run(() => behavior(websocket, cts.Token)); 66 | } 67 | }); 68 | 69 | var completion = app.RunAsync(cts.Token); 70 | return new WebSocketServer(new Uri("ws://localhost:" + port), completion, cts); 71 | } 72 | 73 | public void Dispose() 74 | { 75 | Cancellation.Cancel(); 76 | Completion.Wait(); 77 | } 78 | 79 | public async ValueTask DisposeAsync() 80 | { 81 | Cancellation.Cancel(); 82 | await Completion; 83 | } 84 | 85 | static async Task Echo(WebSocket webSocket, CancellationToken cancellation) 86 | { 87 | while (webSocket.State == WebSocketState.Open && !cancellation.IsCancellationRequested) 88 | { 89 | try 90 | { 91 | var pipe = new Pipe(); 92 | var received = await webSocket.ReceiveAsync(pipe.Writer.GetMemory(512), cancellation).ConfigureAwait(false); 93 | while (!cancellation.IsCancellationRequested && !received.EndOfMessage && received.MessageType != WebSocketMessageType.Close) 94 | { 95 | if (received.Count == 0) 96 | break; 97 | 98 | pipe.Writer.Advance(received.Count); 99 | received = await webSocket.ReceiveAsync(pipe.Writer.GetMemory(512), cancellation).ConfigureAwait(false); 100 | } 101 | 102 | // We didn't get a complete message, we can't flush partial message. 103 | if (cancellation.IsCancellationRequested || !received.EndOfMessage) 104 | break; 105 | 106 | if (received.MessageType == WebSocketMessageType.Close) 107 | { 108 | await webSocket.CloseOutputAsync(webSocket.CloseStatus ?? WebSocketCloseStatus.NormalClosure, webSocket.CloseStatusDescription, cancellation); 109 | break; 110 | } 111 | 112 | // Advance the EndOfMessage bytes before flushing. 113 | pipe.Writer.Advance(received.Count); 114 | if (await pipe.Writer.FlushAsync(cancellation).ConfigureAwait(false) is var flushed && flushed.IsCompleted) 115 | break; 116 | 117 | // Read what we just wrote with the flush. 118 | if (await pipe.Reader.ReadAsync(cancellation).ConfigureAwait(false) is var read && !read.IsCompleted && !read.IsCanceled) 119 | { 120 | if (read.Buffer.IsSingleSegment) 121 | { 122 | await webSocket.SendAsync(read.Buffer.First, WebSocketMessageType.Binary, true, cancellation); 123 | } 124 | else 125 | { 126 | var enumerator = read.Buffer.GetEnumerator(); 127 | var done = !enumerator.MoveNext(); 128 | while (!done) 129 | { 130 | done = !enumerator.MoveNext(); 131 | 132 | // NOTE: we don't use the cancellation here because we don't want to send 133 | // partial messages from an already completely read buffer. 134 | if (done) 135 | await webSocket.SendAsync(enumerator.Current, WebSocketMessageType.Binary, true, cancellation); 136 | else 137 | await webSocket.SendAsync(enumerator.Current, WebSocketMessageType.Binary, false, cancellation); 138 | } 139 | } 140 | pipe.Reader.AdvanceTo(read.Buffer.End); 141 | } 142 | } 143 | catch (Exception ex) when (ex is OperationCanceledException || 144 | ex is WebSocketException || 145 | ex is InvalidOperationException) 146 | { 147 | break; 148 | } 149 | } 150 | } 151 | 152 | record LoggingProvider(ITestOutputHelper Output) : ILoggerProvider 153 | { 154 | public ILogger CreateLogger(string categoryName) => new OutputLogger(Output); 155 | public void Dispose() { } 156 | record OutputLogger(ITestOutputHelper Output) : ILogger 157 | { 158 | public IDisposable BeginScope(TState state) => NullDisposable.Default; 159 | public bool IsEnabled(LogLevel logLevel) => true; 160 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) => 161 | Output.WriteLine($"{logLevel.ToString().Substring(0, 4)}: {formatter.Invoke(state, exception)}"); 162 | } 163 | } 164 | 165 | class NullDisposable : IDisposable 166 | { 167 | public static IDisposable Default { get; } = new NullDisposable(); 168 | NullDisposable() { } 169 | public void Dispose() { } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/Tests/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "methodDisplay": "method" 3 | } 4 | -------------------------------------------------------------------------------- /src/WebSocketChannel/Visibility.cs: -------------------------------------------------------------------------------- 1 | namespace Devlooped.Net 2 | { 3 | public static partial class WebSocketChannel { } 4 | } 5 | 6 | namespace System.Net.WebSockets 7 | { 8 | public static partial class WebSocketExtensions { } 9 | } -------------------------------------------------------------------------------- /src/WebSocketChannel/WebSocketChannel.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | Devlooped.Net 6 | disable 7 | WebSocketChannel 8 | High-performance System.Threading.Channels API adapter for System.Net.WebSockets 9 | readme.md 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/WebSocketChannel/WebSocketExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | #region License 3 | // MIT License 4 | // 5 | // Copyright (c) Daniel Cazzulino 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | #endregion 25 | 26 | #nullable enable 27 | using System.ComponentModel; 28 | using System.Threading.Channels; 29 | using Devlooped.Net; 30 | 31 | namespace System.Net.WebSockets; 32 | 33 | /// 34 | /// Provides the extension method for 35 | /// reading/writing to a using the 36 | /// API. 37 | /// 38 | [EditorBrowsable(EditorBrowsableState.Never)] 39 | static partial class WebSocketExtensions 40 | { 41 | /// 42 | /// Creates a channel over the given for reading/writing 43 | /// purposes. 44 | /// 45 | /// The to create the channel over. 46 | /// Optional friendly name to identify this channel while debugging or troubleshooting. 47 | /// A channel to read/write the given . 48 | public static Channel> CreateChannel(this WebSocket webSocket, string? displayName = default) 49 | => WebSocketChannel.Create(webSocket, displayName); 50 | } 51 | 52 | -------------------------------------------------------------------------------- /src/WebSocketChannel/readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devlooped/WebSocketChannel/5b9d995dc1e7503d0e9d56f23cb22e9aa0c5bbfa/src/icon.png -------------------------------------------------------------------------------- /src/kzu.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devlooped/WebSocketChannel/5b9d995dc1e7503d0e9d56f23cb22e9aa0c5bbfa/src/kzu.snk -------------------------------------------------------------------------------- /src/nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | --------------------------------------------------------------------------------