Skip to content

Commit 773289b

Browse files
authored
Merge pull request #148 from Astn/finish-netstandard-upgrade
2.0: byte-first core, pluggable serializers, Kestrel host (finishes the .NET Standard upgrade from #90)
2 parents 82d23f1 + 0743b09 commit 773289b

127 files changed

Lines changed: 23845 additions & 915 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/build_publish_master.yml‎

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,29 @@ on:
88
jobs:
99
build:
1010

11-
strategy:
12-
matrix:
13-
os: ['ubuntu-latest']
14-
dotnet-version: ['3.1.201']
15-
project : ['Json-Rpc']
16-
17-
runs-on: ${{ matrix.os }}
11+
runs-on: ubuntu-latest
1812

1913
steps:
20-
- uses: actions/checkout@v2
21-
- name: Setup .NET Core
22-
uses: actions/setup-dotnet@v1
14+
- uses: actions/checkout@v7
15+
- name: Setup .NET
16+
uses: actions/setup-dotnet@v6
2317
with:
24-
dotnet-version: ${{matrix.dotnet-version}}
18+
# global.json pins the SDK; the 8.0 runtime is needed for the net8.0 test target.
19+
global-json-file: global.json
20+
dotnet-version: |
21+
8.0.x
22+
10.0.x
2523
- name: Install dependencies
26-
run: dotnet restore
24+
run: dotnet restore AustinHarris.JsonRpc.sln
25+
# Building the solution packs every package project (GeneratePackageOnBuild); `dotnet pack` on the
26+
# solution would trip NU5026 with GeneratePackageOnBuild, so the packages come from the build.
2727
- name: Build
28-
run: dotnet build ${{matrix.project}} --configuration Release
28+
run: dotnet build AustinHarris.JsonRpc.sln --configuration Release --no-restore
2929
- name: Test
30-
run: dotnet test AustinHarris.JsonRpcTestN
31-
# Publish
30+
run: dotnet test AustinHarris.JsonRpcTestN --configuration Release --no-build
31+
# Publish all four packages: the core and the three companions (Json.NET, System.Text.Json, ASP.NET Core).
3232
- name: publish nuget version change
33-
run: dotnet nuget push Json-Rpc/bin/Release/*.nupkg --skip-duplicate --source "https://www.nuget.org" --api-key ${{secrets.NugetKey}} # API key for the NuGet feed
33+
run: |
34+
for project in Json-Rpc AustinHarris.JsonRpc.Newtonsoft AustinHarris.JsonRpc.SystemTextJson AustinHarris.JsonRpc.AspNetCore; do
35+
dotnet nuget push "$project/bin/Release/"*.nupkg --skip-duplicate --source "https://api.nuget.org/v3/index.json" --api-key ${{ secrets.NugetKey }} # API key for the NuGet feed
36+
done
Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Pull Reqest
1+
name: Pull Request
22
on:
33
pull_request:
44
paths-ignore:
@@ -11,25 +11,34 @@ jobs:
1111
strategy:
1212
matrix:
1313
os: ['windows-latest','ubuntu-latest']
14-
dotnet-version: ['3.1.201']
15-
project : ['Json-Rpc']
1614

1715
runs-on: ${{ matrix.os }}
1816

1917
steps:
20-
- uses: actions/checkout@v2
21-
- name: Setup .NET Core
22-
uses: actions/setup-dotnet@v1
18+
- uses: actions/checkout@v7
19+
- name: Setup .NET
20+
uses: actions/setup-dotnet@v6
2321
with:
24-
dotnet-version: ${{matrix.dotnet-version}}
22+
# global.json pins the SDK; the 8.0 runtime is needed for the net8.0 test target.
23+
global-json-file: global.json
24+
dotnet-version: |
25+
8.0.x
26+
10.0.x
2527
- name: Install dependencies
26-
run: dotnet restore
28+
run: dotnet restore AustinHarris.JsonRpc.sln
29+
# Building the solution packs every package project (GeneratePackageOnBuild); `dotnet pack` on the
30+
# solution would trip NU5026 with GeneratePackageOnBuild, so the packages come from the build.
2731
- name: Build
28-
run: dotnet build ${{matrix.project}} --configuration Release --version-suffix ci-${{ github.run_id }}-${{ github.run_number }}
32+
run: dotnet build AustinHarris.JsonRpc.sln --configuration Release --no-restore --version-suffix ci-${{ github.run_id }}-${{ github.run_number }}
2933
- name: Test
30-
run: dotnet test AustinHarris.JsonRpcTestN
31-
# Publish
34+
run: dotnet test AustinHarris.JsonRpcTestN --configuration Release --no-build
35+
# Publish all four pre-release packages: the core and the three companions (Json.NET, System.Text.Json, ASP.NET Core).
36+
# Non-fatal: the build and tests are the pull-request verdict; a rejected key (403) or a fork's missing secret
37+
# shows as a warning here and fails loudly in the master workflow instead.
3238
- name: publish nuget version change
33-
if: ${{matrix.os == 'ubuntu-latest'}}
34-
run: dotnet nuget push Json-Rpc/bin/Release/*.nupkg --skip-duplicate --source "https://www.nuget.org" --api-key ${{secrets.NugetKey}} # API key for the NuGet feed
35-
39+
if: ${{ matrix.os == 'ubuntu-latest' }}
40+
continue-on-error: true
41+
run: |
42+
for project in Json-Rpc AustinHarris.JsonRpc.Newtonsoft AustinHarris.JsonRpc.SystemTextJson AustinHarris.JsonRpc.AspNetCore; do
43+
dotnet nuget push "$project/bin/Release/"*.nupkg --skip-duplicate --source "https://api.nuget.org/v3/index.json" --api-key ${{ secrets.NugetKey }} # API key for the NuGet feed
44+
done

‎.github/workflows/charts.yml‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Benchmark charts
2+
# The committed charts, the explorer page and the README figures must agree with benchmarks/charts/benchmarks.json.
3+
on:
4+
pull_request:
5+
paths:
6+
- "benchmarks/charts/**"
7+
- "README.md"
8+
- "samples/WasmHost/README.md"
9+
push:
10+
branches: [ master ]
11+
paths:
12+
- "benchmarks/charts/**"
13+
- "README.md"
14+
- "samples/WasmHost/README.md"
15+
16+
jobs:
17+
check:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- uses: actions/checkout@v7
21+
- name: Renderer tests
22+
run: python3 -m unittest benchmarks/charts/test_render.py
23+
- name: Committed outputs and README figures match the data
24+
run: python3 benchmarks/charts/render.py --check

‎.github/workflows/pages.yml‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Benchmark explorer (GitHub Pages)
2+
on:
3+
push:
4+
branches: [ master ]
5+
paths:
6+
- "benchmarks/charts/**"
7+
- ".github/workflows/pages.yml"
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
pages: write
13+
id-token: write
14+
15+
concurrency:
16+
group: pages
17+
cancel-in-progress: true
18+
19+
jobs:
20+
deploy:
21+
runs-on: ubuntu-latest
22+
environment:
23+
name: github-pages
24+
url: ${{ steps.deployment.outputs.page_url }}
25+
steps:
26+
- uses: actions/checkout@v7
27+
# The committed outputs must match the data: a stale chart or explorer fails the deploy.
28+
- name: Check the committed charts against the data
29+
run: python3 benchmarks/charts/render.py --check
30+
- name: Stage the site
31+
run: |
32+
mkdir -p site
33+
cp benchmarks/charts/explorer.html site/index.html
34+
cp benchmarks/charts/*.svg benchmarks/charts/*.json site/
35+
- uses: actions/configure-pages@v6
36+
- uses: actions/upload-pages-artifact@v5
37+
with:
38+
path: site
39+
- id: deployment
40+
uses: actions/deploy-pages@v5

‎.gitignore‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,9 @@ Backup*/
108108
UpgradeLog*.XML
109109
.nuget/NuGet.exe
110110
.vs/
111+
112+
# JetBrains Rider / IntelliJ
113+
.idea/
114+
115+
# Python renderer caches
116+
__pycache__/
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<Company>Austin Harris</Company>
5+
<Authors>Austin Harris</Authors>
6+
<Product>Json-Rpc.Net ASP.NET Core host</Product>
7+
<Description>ASP.NET Core / Kestrel hosting for JSON-RPC.Net: MapJsonRpc endpoint (PipeReader in, BodyWriter out, no strings), a raw Kestrel ConnectionHandler for JSON-RPC over TCP, and DI registration of services.</Description>
8+
<VersionPrefix>2.0.0</VersionPrefix>
9+
<VersionSuffix>$(VersionSuffix)</VersionSuffix>
10+
<Copyright>Austin Harris</Copyright>
11+
<PackageProjectUrl>https://github.com/Astn/JSON-RPC.NET</PackageProjectUrl>
12+
<RepositoryUrl>https://github.com/Astn/JSON-RPC.NET</RepositoryUrl>
13+
<RepositoryType>git</RepositoryType>
14+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
15+
<PackageReadmeFile>README.md</PackageReadmeFile>
16+
<PackageTags>json-rpc;jsonrpc;json;rpc;server;aspnetcore;kestrel;pipelines</PackageTags>
17+
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
18+
<LangVersion>latest</LangVersion>
19+
<Nullable>disable</Nullable>
20+
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
21+
<RootNamespace>AustinHarris.JsonRpc.AspNetCore</RootNamespace>
22+
</PropertyGroup>
23+
24+
<ItemGroup>
25+
<None Include="README.md" Pack="true" PackagePath="\" />
26+
</ItemGroup>
27+
28+
<ItemGroup>
29+
<FrameworkReference Include="Microsoft.AspNetCore.App" />
30+
</ItemGroup>
31+
32+
<ItemGroup>
33+
<ProjectReference Include="..\Json-Rpc\AustinHarris.JsonRpc.csproj" />
34+
</ItemGroup>
35+
36+
</Project>
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using AustinHarris.JsonRpc.Serialization;
4+
using Microsoft.AspNetCore.Connections;
5+
6+
namespace AustinHarris.JsonRpc.AspNetCore
7+
{
8+
public partial class JsonRpcConnectionHandler
9+
{
10+
private async Task RunAsynchronousMethodsAsync(ConnectionContext connection)
11+
{
12+
var input = connection.Transport.Input;
13+
var output = connection.Transport.Output;
14+
var token = connection.ConnectionClosed;
15+
string session = _options.SessionId ?? Handler.DefaultSessionId();
16+
// Keep result commits separate from transport flushes: a method can finish on another
17+
// thread while an earlier reply is waiting for transport backpressure.
18+
using var reply = new PooledByteBufferWriter();
19+
try
20+
{
21+
while (true)
22+
{
23+
var result = await input.ReadAsync(token).ConfigureAwait(false);
24+
var buffer = result.Buffer;
25+
bool wrote = false;
26+
try
27+
{
28+
while (JsonFramer.TryReadDocument(ref buffer, out var document))
29+
{
30+
token.ThrowIfCancellationRequested();
31+
if (document.Length <= 1 && document.First.Span[0] != (byte)'{' && document.First.Span[0] != (byte)'[') continue;
32+
if (document.Length > _options.MaxRequestBytes)
33+
{
34+
connection.Abort(new ConnectionAbortedException("JSON-RPC document exceeds MaxRequestBytes."));
35+
return;
36+
}
37+
reply.Clear();
38+
var pending = JsonRpcProcessor.ProcessAsync(session, document, reply, connection, _options.Serializer, token);
39+
if (!pending.IsCompleted && wrote)
40+
{
41+
bool closed = false;
42+
try
43+
{
44+
var flush = await output.FlushAsync(token).ConfigureAwait(false);
45+
closed = flush.IsCompleted || flush.IsCanceled;
46+
wrote = false;
47+
}
48+
finally
49+
{
50+
// Even a failed flush cannot release the input or reply while invocation runs.
51+
await pending.ConfigureAwait(false);
52+
}
53+
if (closed) return;
54+
}
55+
else await pending.ConfigureAwait(false);
56+
token.ThrowIfCancellationRequested();
57+
if (reply.WrittenCount != 0) { reply.CopyTo(output); wrote = true; }
58+
}
59+
if (wrote)
60+
{
61+
var flush = await output.FlushAsync(token).ConfigureAwait(false);
62+
if (flush.IsCompleted || flush.IsCanceled) return;
63+
}
64+
if (result.IsCompleted || result.IsCanceled) return;
65+
if (buffer.Length > _options.MaxRequestBytes)
66+
{
67+
connection.Abort(new ConnectionAbortedException("JSON-RPC document exceeds MaxRequestBytes."));
68+
return;
69+
}
70+
}
71+
finally { input.AdvanceTo(buffer.Start, buffer.End); }
72+
}
73+
}
74+
catch (OperationCanceledException) when (token.IsCancellationRequested) { }
75+
catch (ConnectionAbortedException) { }
76+
}
77+
}
78+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using System;
2+
using System.Buffers;
3+
using System.Threading.Tasks;
4+
using AustinHarris.JsonRpc.Serialization;
5+
using Microsoft.AspNetCore.Connections;
6+
using Microsoft.Extensions.Options;
7+
8+
namespace AustinHarris.JsonRpc.AspNetCore
9+
{
10+
/// <summary>
11+
/// JSON-RPC over a raw Kestrel connection (TCP, Unix socket, named pipe): clients send JSON documents back to
12+
/// back (optionally whitespace / newline separated) and receive responses in order. Wire it up with
13+
/// <c>kestrel.ListenLocalhost(port, l => l.UseConnectionHandler&lt;JsonRpcConnectionHandler&gt;())</c>.
14+
/// The connection's <see cref="ConnectionContext"/> is the RPC context for every call.
15+
/// </summary>
16+
public partial class JsonRpcConnectionHandler : ConnectionHandler
17+
{
18+
private readonly JsonRpcOptions _options;
19+
20+
public JsonRpcConnectionHandler(IOptions<JsonRpcOptions> options)
21+
{
22+
_options = options?.Value ?? new JsonRpcOptions();
23+
}
24+
25+
/// <summary>Processes a connection using the hosting mode selected in options.</summary>
26+
public override Task OnConnectedAsync(ConnectionContext connection)
27+
{
28+
return _options.EnableAsyncMethods ? RunAsynchronousMethodsAsync(connection) : RunSynchronousMethodsAsync(connection);
29+
}
30+
31+
private async Task RunSynchronousMethodsAsync(ConnectionContext connection)
32+
{
33+
var input = connection.Transport.Input;
34+
var output = connection.Transport.Output;
35+
string session = _options.SessionId ?? Handler.DefaultSessionId();
36+
37+
while (true)
38+
{
39+
var result = await input.ReadAsync(connection.ConnectionClosed).ConfigureAwait(false);
40+
var buffer = result.Buffer;
41+
bool wrote = false;
42+
43+
while (JsonFramer.TryReadDocument(ref buffer, out var document))
44+
{
45+
// the framer hands back a one-byte slice for anything that is not '{' or '[': drop it
46+
if (document.Length > 1 || document.First.Span[0] == (byte)'{' || document.First.Span[0] == (byte)'[')
47+
{
48+
if (document.Length > _options.MaxRequestBytes)
49+
{
50+
connection.Abort(new ConnectionAbortedException("JSON-RPC document exceeds MaxRequestBytes."));
51+
return;
52+
}
53+
JsonRpcProcessor.Process(session, in document, output, connection, _options.Serializer);
54+
wrote = true;
55+
}
56+
}
57+
58+
if (wrote)
59+
{
60+
var flush = await output.FlushAsync(connection.ConnectionClosed).ConfigureAwait(false);
61+
if (flush.IsCompleted || flush.IsCanceled) break;
62+
}
63+
64+
if (result.IsCompleted || result.IsCanceled)
65+
{
66+
input.AdvanceTo(buffer.Start, buffer.End);
67+
break;
68+
}
69+
if (buffer.Length > _options.MaxRequestBytes)
70+
{
71+
connection.Abort(new ConnectionAbortedException("JSON-RPC document exceeds MaxRequestBytes."));
72+
return;
73+
}
74+
// consumed up to the last complete document, examined everything
75+
input.AdvanceTo(buffer.Start, buffer.End);
76+
}
77+
}
78+
}
79+
}

0 commit comments

Comments
 (0)