├── src
├── Tests
│ ├── xunit.runner.json
│ ├── Properties
│ │ └── launchSettings.json
│ ├── Tests.csproj
│ └── WebSocketServer.cs
├── icon.png
├── kzu.snk
├── SponsorLink
│ ├── Library
│ │ ├── MyClass.cs
│ │ ├── readme.md
│ │ ├── Library.csproj
│ │ └── Resources.resx
│ ├── Tests
│ │ ├── keys
│ │ │ ├── kzu.key
│ │ │ ├── kzu.pub
│ │ │ ├── kzu.pub.txt
│ │ │ ├── kzu.pub.jwk
│ │ │ ├── sponsorlink.jwt
│ │ │ ├── kzu.key.txt
│ │ │ └── kzu.key.jwk
│ │ ├── .netconfig
│ │ ├── Attributes.cs
│ │ ├── Extensions.cs
│ │ ├── JsonOptions.cs
│ │ ├── Sample.cs
│ │ ├── Tests.csproj
│ │ ├── Resources.resx
│ │ └── SponsorManifestTests.cs
│ ├── jwk.ps1
│ ├── SponsorLink
│ │ ├── sponsorable.md
│ │ ├── AnalyzerOptionsExtensions.cs
│ │ ├── AppDomainDictionary.cs
│ │ ├── Tracing.cs
│ │ ├── SponsorStatus.cs
│ │ ├── SponsorableLib.targets
│ │ ├── SponsorLinkAnalyzer.cs
│ │ ├── SponsorLink.csproj
│ │ ├── SponsorManifest.cs
│ │ ├── buildTransitive
│ │ │ └── Devlooped.Sponsors.targets
│ │ ├── SponsorLink.cs
│ │ ├── Resources.es-AR.resx
│ │ ├── Resources.resx
│ │ └── Resources.es.resx
│ ├── Analyzer
│ │ ├── Properties
│ │ │ └── launchSettings.json
│ │ ├── buildTransitive
│ │ │ └── SponsorableLib.targets
│ │ ├── StatusReportingGenerator.cs
│ │ ├── Analyzer.csproj
│ │ ├── GraceApiAnalyzer.cs
│ │ └── StatusReportingAnalyzer.cs
│ ├── Directory.Build.targets
│ ├── Directory.Build.props
│ ├── readme.md
│ ├── SponsorLinkAnalyzer.sln
│ └── SponsorLink.Analyzer.Tests.targets
├── Directory.props
├── CodeAnalysis
│ ├── WebSocketChannel.targets
│ └── CodeAnalysis.csproj
├── WebSocketChannel
│ ├── readme.md
│ ├── Visibility.cs
│ ├── WebSocketChannel.csproj
│ └── WebSocketExtensions.cs
├── Benchmark
│ ├── Benchmark.csproj
│ └── Program.cs
├── nuget.config
└── Directory.Build.props
├── _config.yml
├── assets
├── img
│ ├── icon.png
│ ├── noun_web_849841.svg
│ ├── noun_TV_2679303.svg
│ └── icon.svg
└── css
│ └── style.scss
├── global.json
├── Directory.Build.rsp
├── .gitattributes
├── .github
├── workflows
│ ├── changelog.config
│ ├── dotnet-file.yml
│ ├── changelog.yml
│ ├── includes.yml
│ ├── publish.yml
│ ├── build.yml
│ └── triage.yml
├── release.yml
├── dependabot.yml
└── actions
│ └── dotnet
│ └── action.yml
├── .gitignore
├── license.txt
├── WebSocketChannel.sln
├── changelog.md
├── .editorconfig
└── readme.md
/src/Tests/xunit.runner.json:
--------------------------------------------------------------------------------
1 | {
2 | "methodDisplay": "method"
3 | }
4 |
--------------------------------------------------------------------------------
/src/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devlooped/WebSocketChannel/HEAD/src/icon.png
--------------------------------------------------------------------------------
/src/kzu.snk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devlooped/WebSocketChannel/HEAD/src/kzu.snk
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-slate
2 |
3 | exclude: [ 'src/', '*.sln', 'Gemfile*', '*.rsp' ]
--------------------------------------------------------------------------------
/assets/img/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devlooped/WebSocketChannel/HEAD/assets/img/icon.png
--------------------------------------------------------------------------------
/src/SponsorLink/Library/MyClass.cs:
--------------------------------------------------------------------------------
1 | namespace SponsorableLib;
2 |
3 | public class MyClass
4 | {
5 | }
6 |
--------------------------------------------------------------------------------
/src/Directory.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | WebSocketChannel
4 |
5 |
--------------------------------------------------------------------------------
/src/SponsorLink/Tests/keys/kzu.key:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devlooped/WebSocketChannel/HEAD/src/SponsorLink/Tests/keys/kzu.key
--------------------------------------------------------------------------------
/src/SponsorLink/Tests/keys/kzu.pub:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devlooped/WebSocketChannel/HEAD/src/SponsorLink/Tests/keys/kzu.pub
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "allowPrerelease": true,
4 | "rollForward": "latestPatch",
5 | "version": "6.0.100-preview.*"
6 | }
7 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/CodeAnalysis/WebSocketChannel.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/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/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/WebSocketChannel/readme.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/Directory.Build.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/src/SponsorLink/Analyzer/buildTransitive/SponsorableLib.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/SponsorLink/Tests/keys/kzu.pub.txt:
--------------------------------------------------------------------------------
1 | MIIBigKCAYEAyP71VgOgHDtGbxdyN31mIFFITmGYEk2cwepKbyqKTbTYXF1OXaMoP5n3mfwqwzUQmEAsrclAigPcK4GIy5WWlc5YujIxKauJjsKe0FBxMnFp9o1UcBUHfgDJjaAKieQxb44717b1MwCcflEGnCGTXkntdr45y9Gi1D9+oBw5zIVZekgMP55XxmKvkJd1k+bYWSv+QFG2JJwRIGwr29Jr62juCsLB7Tg83ZGKCa22Y/7lQcezxRRD5OrGWhf3gTYArbrEzbYy653zbHfbOCJeVBe/bXDkR74yG3mmq/Ne0qhNk6wXuX+NrKEvdPxRSRBF7C465fcVY9PM6eTqEPQwKDiarHpU1NTwUetzb+YKry+h678RJWMhC7I9lzzWVobbC0YVKG7XpeVqBB4u7Q6cGo5Xkf19VldkIxQMu9sFeuHGDSoiCLqmRmwNn9GsMV77oZWr+OPrxEdZzL9BcI4fMJMz7YdiIu+qbIp/vqatbalfNasumf8RgtPOkR2vgc59AgMBAAE=
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 }}
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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 | }
--------------------------------------------------------------------------------
/assets/img/noun_web_849841.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/.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/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/assets/img/noun_TV_2679303.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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/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/nuget.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/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/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/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.
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/assets/img/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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.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/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/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/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/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/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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."
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |  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 | [](https://www.nuget.org/packages/WebSocketChannel)
7 | [](https://www.nuget.org/packages/WebSocketChannel)
8 | [](https://github.com/devlooped/WebSocketChannel/blob/main/license.txt)
9 | [](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 | [](https://pkg.kzu.app/index.json)
113 | [](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 | [](https://github.com/clarius)
130 | [](https://github.com/MFB-Technologies-Inc)
131 | [](https://github.com/torutek-gh)
132 | [](https://github.com/drivenet)
133 | [](https://github.com/Keflon)
134 | [](https://github.com/tbolon)
135 | [](https://github.com/kfrancis)
136 | [](https://github.com/twenzel)
137 | [](https://github.com/unoplatform)
138 | [](https://github.com/dansiegel)
139 | [](https://github.com/rbnswartz)
140 | [](https://github.com/jfoshee)
141 | [](https://github.com/Mrxx99)
142 | [](https://github.com/eajhnsn1)
143 | [](https://github.com/IxTechnologies)
144 | [](https://github.com/davidjenni)
145 | [](https://github.com/Jonathan-Hickey)
146 | [](https://github.com/akunzai)
147 | [](https://github.com/KenBonny)
148 | [](https://github.com/SimonCropp)
149 | [](https://github.com/agileworks-eu)
150 | [](https://github.com/sorahex)
151 | [](https://github.com/arsdragonfly)
152 | [](https://github.com/vezel-dev)
153 | [](https://github.com/ChilliCream)
154 | [](https://github.com/4OTC)
155 | [](https://github.com/v-limo)
156 | [](https://github.com/jordansjones)
157 | [](https://github.com/DominicSchell)
158 |
159 |
160 |
161 |
162 | [](https://github.com/sponsors/devlooped)
163 |
164 |
165 | [Learn more about GitHub Sponsors](https://github.com/sponsors)
166 |
167 |
168 |
--------------------------------------------------------------------------------
/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/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.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/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 |
--------------------------------------------------------------------------------