Skip to content

Commit de7c739

Browse files
authored
Merge pull request #149 from Astn/docs-site
Documentation site and editorial pass
2 parents 773289b + 297c177 commit de7c739

13 files changed

Lines changed: 1128 additions & 299 deletions

File tree

‎.github/workflows/pages.yml‎

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1-
name: Benchmark explorer (GitHub Pages)
1+
name: Documentation site (GitHub Pages)
22
on:
33
push:
44
branches: [ master ]
55
paths:
6+
- "README.md"
7+
- "docs/**"
8+
- "site/**"
9+
- "AustinHarris.JsonRpc.AspNetCore/README.md"
10+
- "AustinHarris.JsonRpc.Newtonsoft/README.md"
11+
- "AustinHarris.JsonRpc.SystemTextJson/README.md"
12+
- "samples/WasmHost/README.md"
13+
- "benchmarks/Micro/README.md"
614
- "benchmarks/charts/**"
715
- ".github/workflows/pages.yml"
816
workflow_dispatch:
@@ -24,17 +32,23 @@ jobs:
2432
url: ${{ steps.deployment.outputs.page_url }}
2533
steps:
2634
- uses: actions/checkout@v7
27-
# The committed outputs must match the data: a stale chart or explorer fails the deploy.
35+
- uses: actions/setup-python@v7
36+
with:
37+
python-version: "3.12"
38+
- uses: typst-community/setup-typst@v5
39+
with:
40+
typst-version: "0.14.2"
41+
cache-dependency-path: site/page.typ
42+
- run: pip install pygments==2.19.2
43+
# The committed charts must match the data: a stale chart or explorer fails the deploy.
2844
- name: Check the committed charts against the data
2945
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/
46+
# Every documentation page is rendered from its Markdown source; a page that fails to compile fails here.
47+
- name: Build the site
48+
run: python3 site/build.py --out site/_out
3549
- uses: actions/configure-pages@v6
3650
- uses: actions/upload-pages-artifact@v5
3751
with:
38-
path: site
52+
path: site/_out
3953
- id: deployment
4054
uses: actions/deploy-pages@v5

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,6 @@ UpgradeLog*.XML
114114

115115
# Python renderer caches
116116
__pycache__/
117+
118+
# Documentation site build output (site/build.py)
119+
site/_out/

‎AustinHarris.JsonRpc.AspNetCore/README.md‎

Lines changed: 90 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ UTF-8 bytes, so the HTTP endpoint reads the body with `PipeReader` and writes st
55
`Response.BodyWriter`; nothing is turned into a string on the way through. A `ConnectionHandler` does the
66
same for JSON-RPC over a raw Kestrel connection (TCP, Unix socket, named pipe).
77

8+
## Install
9+
10+
```
11+
dotnet add package AustinHarris.JsonRpc.AspNetCore
12+
```
13+
14+
Targets `net8.0` and `net10.0`; depends on the `AustinHarris.JsonRpc` core package and the ASP.NET Core shared
15+
framework.
16+
817
## HTTP endpoint
918

1019
```csharp
@@ -31,15 +40,16 @@ public class CalculatorService
3140
public CalculatorService(ILogger<CalculatorService> log) => _log = log;
3241

3342
[JsonRpcMethod]
34-
public double add(double l, double r) => l + r;
43+
public double add(double l, double r)
44+
{
45+
_log.LogDebug("add {L} {R}", l, r);
46+
return l + r;
47+
}
3548
}
3649
```
3750

3851
`POST /rpc` with a request or a batch answers `200 application/json`; a notification answers `204`.
3952
Inside a method `JsonRpcContext.Current().Value` is the `HttpContext` (override with `ContextFactory`).
40-
Classes deriving from `JsonRpcService` still bind themselves; `AddJsonRpcService<T>()` is for classes that
41-
take constructor dependencies, and `AddJsonRpcServicesFromAssembly(typeof(Program).Assembly)` registers every
42-
class that declares a `[JsonRpcMethod]`, MVC controllers included.
4353

4454
Because it is an ordinary endpoint, `RequireAuthorization()`, rate limiting, output caching and the rest of
4555
the middleware pipeline compose with it:
@@ -48,35 +58,90 @@ the middleware pipeline compose with it:
4858
app.MapJsonRpc("/rpc").RequireAuthorization("api");
4959
```
5060

51-
## Raw connection (TCP)
61+
`MapJsonRpc` adds no authorization, TLS requirement, rate limit or request deadline by itself; apply those
62+
policies explicitly. `MaxRequestBytes` limits the HTTP body, but there is no batch-count or response-size limit.
63+
Keep `Config.IncludeExceptionDetails` off for untrusted clients; the default still sends an unhandled exception's
64+
CLR type name and message, see [Exception disclosure](https://github.com/Astn/JSON-RPC.NET#exception-disclosure)
65+
in the main README.
66+
67+
`MapJsonRpc(pattern = "/jsonrpc", options = null)` uses the options from `AddJsonRpc` unless you pass your own,
68+
so two endpoints can serve two sessions, for example a strict API next to a lenient one for older clients:
69+
70+
```csharp
71+
app.MapJsonRpc("/rpc");
72+
app.MapJsonRpc("/legacy", new JsonRpcOptions { SessionId = "legacy-clients", Serializer = new NewtonsoftJsonRpcSerializer() });
73+
```
74+
75+
## Services and lifetime
76+
77+
`AddJsonRpcService<T>()` registers `T` as a singleton unless `T` is already registered. When the host starts,
78+
each registered service is resolved once from the root container and bound; that one instance then serves every
79+
HTTP request and every raw connection, concurrently. So `T` and its dependencies must be thread-safe, and `T`
80+
cannot take scoped dependencies such as an EF Core `DbContext`: with scope validation on, the host fails at
81+
startup; with it off, the dependency leaks. For per-request services, resolve them inside the method from
82+
`((HttpContext)Handler.RpcContext()).RequestServices` on HTTP; a raw connection's context is the
83+
`ConnectionContext`, which has no request scope. Do not inject request-scoped state into a service; read
84+
per-request data from the context instead.
85+
86+
`AddJsonRpcServicesFromAssembly(assembly)` does the same for every non-abstract class in the assembly that
87+
declares a `[JsonRpcMethod]`. Private methods count, so the attribute is the whole access list, and an MVC
88+
controller that carries it becomes a singleton too.
89+
90+
A class deriving from `JsonRpcService` binds itself to the default session in its constructor. Registering it here
91+
as well is harmless when the effective session is the default. With `SessionId` set, the host binds it to that
92+
session in addition, so it stays reachable on the default session too.
93+
94+
## Raw connection (TCP, Unix socket, named pipe)
5295

5396
```csharp
5497
builder.WebHost.ConfigureKestrel(k =>
5598
{
56-
k.ListenAnyIP(9000, l => l.UseConnectionHandler<JsonRpcConnectionHandler>());
99+
k.ListenLocalhost(9000, l => l.UseConnectionHandler<JsonRpcConnectionHandler>());
100+
// k.ListenUnixSocket("/tmp/rpc.sock", l => l.UseConnectionHandler<JsonRpcConnectionHandler>());
101+
// k.ListenNamedPipe("rpc", l => l.UseConnectionHandler<JsonRpcConnectionHandler>());
57102
});
58103
```
59104

60-
Clients write JSON documents back to back (a newline between them is fine); each document is answered in
61-
order on the same connection, notifications produce nothing. The `ConnectionContext` is the RPC context.
105+
Clients write JSON documents back to back (whitespace or newlines between them are fine) and read the responses
106+
in the same order, also back to back with no newline or `Content-Length` prefix, so the client must parse one
107+
complete JSON value at a time. Notifications produce nothing. The `ConnectionContext` is the RPC context.
108+
109+
The framer accepts strict JSON only, even with the Json.NET serializer or a lenient `JsmnSerializer` selected.
110+
A document larger than `MaxRequestBytes` aborts the connection. Documents on one connection are processed one at
111+
a time, in order; separate connections run concurrently.
112+
113+
A raw connection does not pass through the HTTP middleware pipeline, so it has no authentication, authorisation
114+
or rate limiting. Listen on loopback or a Unix socket, or configure transport security, authentication and
115+
connection limits at the Kestrel listener or in a surrounding protocol.
116+
117+
## Async methods
118+
119+
Set `EnableAsyncMethods = true` to serve `Task` and `ValueTask` methods through `ProcessAsync`. With it off (the
120+
default), requests are processed synchronously and an async method is answered with `-32603` without being
121+
invoked.
122+
123+
- **HTTP:** the call is cancelled when the client disconnects (`HttpContext.RequestAborted`). Notifications are
124+
awaited and still answer `204`. The body reader stays leased until the invocation finishes.
125+
- **Raw connections:** documents are processed one at a time, in order. Replies already finished are flushed
126+
before the connection waits on a slow method. When the connection closes, the running method is waited for and
127+
its response discarded.
62128

63-
With `EnableAsyncMethods = true`, HTTP awaits `ProcessAsync` with `HttpContext.RequestAborted`;
64-
the body reader remains leased until invocation finishes. Raw connections await each framed document
65-
before starting the next and flush earlier completed replies before waiting for a slow document.
66-
Notifications are awaited and keep the same HTTP status rules. Connection cancellation is cooperative:
67-
the processor waits for the running method to terminate before releasing input and discards its staged response.
68-
Mark a `CancellationToken` parameter with `[JsonRpcCancellation]` to receive that token.
69-
The default mode preserves synchronous processing and rejects async methods at call time.
129+
A method receives the token by declaring a `[JsonRpcCancellation] CancellationToken` parameter; see
130+
[Asynchronous methods and cancellation](https://github.com/Astn/JSON-RPC.NET#asynchronous-methods-and-cancellation)
131+
in the main README.
70132

71133
## Options
72134

73-
| Option | Default | Meaning |
74-
|---|---|---|
75-
| `EnableAsyncMethods` | false | use ProcessAsync for Task/ValueTask methods, with host cancellation |
76-
| `SessionId` | default session | which session's methods answer |
77-
| `SessionSelector` | null | pick the session per HTTP request |
78-
| `Serializer` | session, then `Config.Serializer` | serializer for this host |
79-
| `ContextFactory` | `HttpContext` | what `JsonRpcContext.Current()` returns |
80-
| `MaxRequestBytes` | 4 MB | larger bodies get 413 (HTTP) or abort the connection |
81-
| `ResponseContentType` | `application/json` | |
82-
| `NoContentForNotifications` | true | 204 for notifications, otherwise 200 with an empty body |
135+
| Option | Default | Scope | Meaning |
136+
|---|---|---|---|
137+
| `EnableAsyncMethods` | false | HTTP and raw | use `ProcessAsync` for `Task`/`ValueTask` methods, with host cancellation |
138+
| `SessionId` | default session | HTTP and raw | which session's methods answer |
139+
| `SessionSelector` | null | HTTP | pick the session per request from the `HttpContext`; it must map to a fixed set of ids, because an unknown id creates a session that persists |
140+
| `Serializer` | session, then `Config.Serializer` | HTTP and raw | serializer for this host |
141+
| `ContextFactory` | `HttpContext` | HTTP | what `JsonRpcContext.Current()` returns |
142+
| `MaxRequestBytes` | 4 MB | HTTP body, or one raw document | larger bodies get 413; a larger raw document aborts the connection |
143+
| `ResponseContentType` | `application/json` | HTTP | |
144+
| `NoContentForNotifications` | true | HTTP | 204 for notifications, otherwise 200 with an empty body |
145+
146+
For raw connections the RPC context is always the `ConnectionContext`; `SessionSelector`, `ContextFactory`,
147+
`ResponseContentType` and `NoContentForNotifications` are not used.

‎AustinHarris.JsonRpc.Newtonsoft/README.md‎

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@
22

33
Json.NET (Newtonsoft.Json) serializer for [JSON-RPC.Net](https://github.com/Astn/JSON-RPC.NET) 2.0.
44

5-
The core package (`AustinHarris.JsonRpc`) parses the JSON-RPC envelope itself and ships a small dependency-free
6-
serializer for parameters and results. Install this package when you want Json.NET to do the value conversions:
7-
its converters, contract resolvers, `[JsonProperty]` attributes, date/float handling, and its tolerance for
8-
non-strict JSON.
5+
The core package (`AustinHarris.JsonRpc`) parses the JSON-RPC envelope itself and ships a built-in serializer for
6+
parameters and results that needs no JSON library. Install this package when you want Json.NET to do the value
7+
conversions: its converters, contract resolvers, `[JsonProperty]` attributes, date/float handling, and its
8+
tolerance for non-strict JSON.
99

1010
```
1111
dotnet add package AustinHarris.JsonRpc.Newtonsoft
1212
```
1313

14+
Targets `netstandard2.0`, `netstandard2.1`, `net8.0` and `net10.0`; depends on Newtonsoft.Json 13.0.4 and the
15+
`AustinHarris.JsonRpc` core package.
16+
1417
## Choosing the serializer
1518

1619
Process-wide (every session that does not override it):
@@ -27,8 +30,7 @@ Config.SetSerializer(new NewtonsoftJsonRpcSerializer(settings));
2730
Per session:
2831

2932
```csharp
30-
Handler.GetSessionHandler("session-42").Serializer = new NewtonsoftJsonRpcSerializer(settings);
31-
// or: Config.SetSerializer("session-42", new NewtonsoftJsonRpcSerializer(settings));
33+
Config.SetSerializer("session-42", new NewtonsoftJsonRpcSerializer(settings));
3234
```
3335

3436
Per call (overrides both):
@@ -38,8 +40,14 @@ var serializer = new NewtonsoftJsonRpcSerializer(settings);
3840
string response = JsonRpcProcessor.ProcessSync(sessionId, json, context, serializer);
3941
```
4042

41-
Create the serializer once and reuse it: it holds one `JsonSerializer` built from the settings, and the processor
42-
caches an envelope reader per serializer instance.
43+
When each level is the right one is covered in
44+
[docs/serializers.md](https://github.com/Astn/JSON-RPC.NET/blob/master/docs/serializers.md).
45+
46+
Create the serializer once and reuse it: it holds one `JsonSerializer` built from the settings, and the processor's
47+
synchronous path keeps an envelope reader in per-thread scratch storage for as long as the serializer instance
48+
stays the same, so a new serializer per call throws that reuse away. That `JsonSerializer`, its converters,
49+
contract resolver and callbacks are used concurrently by unrelated requests: configure the instance before serving
50+
traffic, do not mutate it while requests are in flight, and make custom components thread-safe.
4351

4452
## Settings-based helpers (1.x compatibility)
4553

@@ -59,15 +67,20 @@ Each distinct settings instance is turned into a serializer the first time it is
5967
* Every conversion honours the settings: params, results, `error.data`, and the `JsonRequest.Params` handed to
6068
pre/post-process handlers (a `JObject` / `JArray` / primitive, as `JsonConvert.DeserializeObject` returns).
6169
* Json.NET's defaults already match the library's wire conventions: compact output, `3.0` for whole floating
62-
values, ISO-8601 dates (fraction only when non-zero, trailing zeros trimmed) with the offset, `char` as a one-character string, nulls
63-
written, members in declaration order, case-insensitive member names on input, numbers coerced to
64-
`bool`/`char`/floating types.
70+
values, ISO-8601 dates (fraction only when non-zero, trailing zeros trimmed; `Z` for UTC, the offset for Local,
71+
nothing for Unspecified), `char` as a one-character string, nulls written, members in declaration order,
72+
case-insensitive member names on input, numbers coerced to `bool`/`char`/floating types.
6573
* Leniency. Json.NET accepts more than RFC 8259, and with this serializer selected so does the envelope reader:
6674
single-quoted strings, unquoted member names and trailing commas are accepted in the request, e.g.
6775
`{method:'add',params:[1,2],id:1}`. With the built-in serializer the same request is a `-32700` parse error.
76+
Leniency applies to the HTTP endpoint and to in-process calls; on a raw Kestrel connection the framer that
77+
splits the stream into documents accepts strict JSON only.
6878
* `JsonConvert.DefaultSettings`, if your process sets it, is the baseline exactly as it is for `JsonConvert`.
6979

70-
Conversion failures throw and are reported to the client as `-32603 Internal Error`.
80+
A value Json.NET cannot convert to the parameter's type (a `JsonException`, or a format, overflow or cast
81+
exception while reading an argument) is reported as `-32602 Invalid params`, with `data` naming the parameter and
82+
the expected type; the value sent is never echoed. A type the serializer cannot handle at all, or an exception
83+
inside your method, is `-32603 Internal error`.
7184

7285
## Performance notes
7386

0 commit comments

Comments
 (0)