Skip to content

Commit f1593c0

Browse files
committed
Getting started: single-file lambda server first, raw string literals in step 2
Step 1 now opens with a complete Kestrel server in one file: a .NET 10 file-based app that registers two lambdas with ServiceBinder.BindMethod and maps them at /rpc, with the verified curl exchange. The CalculatorService class follows as the second example, and the session paragraph says what happens when both register the same name. Step 2 uses raw string literals and a u8 literal for the byte call, so the JSON is readable and the byte[]/AsSpan() workaround is no longer needed in the example. The note records what was measured: a bare byte[] is ambiguous on C# 12 and 13 and selects the span overload on C# 14 and later. The package README mirrors both changes at shorter length.
1 parent c4cfb1e commit f1593c0

2 files changed

Lines changed: 87 additions & 14 deletions

File tree

‎Json-Rpc/README.md‎

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,42 @@ Coming from 1.x? Read [What is new in 2.0](https://astn.github.io/JSON-RPC.NET/c
2626

2727
### Declare a service
2828

29-
Create `CalculatorService.cs` with the service below.
29+
Save this as `server.cs`, a .NET 10 file-based app: one C# file with no project file.
30+
The `#:sdk` and `#:package` directives select the web SDK and package.
31+
`ServiceBinder.BindMethod` registers lambdas served by Kestrel at `/rpc`.
32+
33+
```csharp
34+
#:sdk Microsoft.NET.Sdk.Web
35+
#:package AustinHarris.JsonRpc.AspNetCore@2.0.0-preview.1
36+
37+
using AustinHarris.JsonRpc;
38+
using AustinHarris.JsonRpc.AspNetCore;
39+
40+
ServiceBinder.BindMethod("add", (double l, double r) => l + r);
41+
ServiceBinder.BindMethod("greet", (string who) => "hello " + who);
42+
43+
var builder = WebApplication.CreateBuilder(args);
44+
builder.Services.AddJsonRpc();
45+
46+
var app = builder.Build();
47+
app.MapJsonRpc("/rpc");
48+
app.Run();
49+
```
50+
51+
Run `dotnet run server.cs`; Kestrel prints its listening URL.
52+
Use `dotnet run server.cs -- --urls http://127.0.0.1:5077` to pin it for this request from another terminal:
53+
54+
```bash
55+
curl -s -X POST http://127.0.0.1:5077/rpc -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"add","params":[1,2],"id":1}'
56+
```
57+
58+
```json
59+
{"jsonrpc":"2.0","result":3.0,"id":1}
60+
```
61+
62+
On .NET 8, use the same code in `Program.cs` in an ordinary ASP.NET Core project, install with `dotnet add package AustinHarris.JsonRpc.AspNetCore --prerelease`, and drop the two `#:` lines.
63+
64+
For a service class, create `CalculatorService.cs`.
3065
Derive from `JsonRpcService` and mark exposed methods with `[JsonRpcMethod]`.
3166
Constructing the service registers its methods in the default session.
3267

@@ -47,8 +82,13 @@ public class CalculatorService : JsonRpcService
4782
```
4883

4984
Methods can be `private`. Parameters can be positional or named.
85+
Optional parameter defaults are honoured; `[JsonRpcParam("name")]` overrides a parameter's JSON name.
5086
Keep the service instance alive; it serves concurrent requests, so its state must be thread-safe.
5187

88+
Both examples use the default session (`Handler.DefaultSessionId()`).
89+
Lambdas and classes can be mixed in one session when their method names differ; both examples register `add`, so keep one of them.
90+
The next step drives `CalculatorService` in process, without a transport.
91+
5292
### Process requests
5393

5494
Put this code in `Program.cs` in a console project targeting `net8.0` or `net10.0`.
@@ -64,17 +104,16 @@ using AustinHarris.JsonRpc;
64104
var service = new CalculatorService(); // binds itself to the default session; keep a reference
65105
66106
// Strings, asynchronous invocation.
67-
string response = await JsonRpcProcessor.ProcessAsync("{\"jsonrpc\":\"2.0\",\"method\":\"add\",\"params\":[1,2],\"id\":1}");
107+
string response = await JsonRpcProcessor.ProcessAsync("""{"jsonrpc":"2.0","method":"add","params":[1,2],"id":1}""");
68108
// {"jsonrpc":"2.0","result":3.0,"id":1}
69109
70110
// Strings, synchronous, on the calling thread. Named parameters.
71-
string sync = JsonRpcProcessor.ProcessSync("{\"method\":\"multiply\",\"params\":{\"l\":6,\"r\":7},\"id\":2}");
111+
string sync = JsonRpcProcessor.ProcessSync("""{"method":"multiply","params":{"l":6,"r":7},"id":2}""");
72112
// {"jsonrpc":"2.0","result":42,"id":2}
73113
74114
// Bytes: the native path. The string overloads transcode into it.
75-
byte[] request = Encoding.UTF8.GetBytes("{\"method\":\"add\",\"params\":[2,3],\"id\":3}");
76115
var output = new ArrayBufferWriter<byte>();
77-
JsonRpcProcessor.Process(Handler.DefaultSessionId(), request.AsSpan(), output);
116+
JsonRpcProcessor.Process(Handler.DefaultSessionId(), """{"method":"add","params":[2,3],"id":3}"""u8, output);
78117
Console.WriteLine(Encoding.UTF8.GetString(output.WrittenSpan)); // nothing is written for a notification
79118
```
80119

@@ -86,7 +125,8 @@ The string responses appear in the comments above. The byte call prints:
86125

87126
A batch returns an array when it contains calls that need responses.
88127
A notification has no `id` and produces no response.
89-
Pass a `byte[]` as `AsSpan()` to select the span overload.
128+
A `"""..."""u8` literal is a `ReadOnlySpan<byte>` (C# 11 and later).
129+
A bare `byte[]` is ambiguous between the memory and span overloads on C# 12 and 13; pass it as `AsSpan()` there.
90130

91131
Use `JsonRpcProcessor.ProcessAsync` for methods returning `Task` or `ValueTask`.
92132
The synchronous entry points do not await those methods.

‎README.md‎

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,39 @@ Add `AustinHarris.JsonRpc.Newtonsoft` or `AustinHarris.JsonRpc.SystemTextJson` i
9191

9292
### 1. Declare a service
9393

94-
Derive from `JsonRpcService` and mark the methods you want to expose with `[JsonRpcMethod]`. Constructing the service registers it, so you only need to keep the instance alive.
94+
Save this as `server.cs`. It is a .NET 10 file-based app: one C# file with no project file. The `#:sdk` and `#:package` directives select the web SDK and package. `ServiceBinder.BindMethod` registers the lambdas; Kestrel serves them at `/rpc`.
95+
96+
```csharp
97+
#:sdk Microsoft.NET.Sdk.Web
98+
#:package AustinHarris.JsonRpc.AspNetCore@2.0.0-preview.1
99+
100+
using AustinHarris.JsonRpc;
101+
using AustinHarris.JsonRpc.AspNetCore;
102+
103+
ServiceBinder.BindMethod("add", (double l, double r) => l + r);
104+
ServiceBinder.BindMethod("greet", (string who) => "hello " + who);
105+
106+
var builder = WebApplication.CreateBuilder(args);
107+
builder.Services.AddJsonRpc();
108+
109+
var app = builder.Build();
110+
app.MapJsonRpc("/rpc");
111+
app.Run();
112+
```
113+
114+
Run `dotnet run server.cs`. Kestrel prints the URL it listens on. To use the address below, run `dotnet run server.cs -- --urls http://127.0.0.1:5077`, then send this request from another terminal:
115+
116+
```bash
117+
curl -s -X POST http://127.0.0.1:5077/rpc -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"add","params":[1,2],"id":1}'
118+
```
119+
120+
```json
121+
{"jsonrpc":"2.0","result":3.0,"id":1}
122+
```
123+
124+
On .NET 8, the same code works in `Program.cs` in an ordinary ASP.NET Core project: install the package with `dotnet add package AustinHarris.JsonRpc.AspNetCore --prerelease` and drop the two `#:` lines.
125+
126+
For methods grouped in a class, create `CalculatorService.cs`. Derive from `JsonRpcService` and mark the methods you want to expose with `[JsonRpcMethod]`. Constructing the service registers it, so you only need to keep the instance alive.
95127

96128
```csharp
97129
using AustinHarris.JsonRpc;
@@ -111,7 +143,9 @@ public class CalculatorService : JsonRpcService
111143

112144
Methods can be `private`; parameters may be positional (`"params":[1,2]`) or named (`"params":{"l":1,"r":2}`). Optional parameters with default values are honoured, and a parameter's JSON name can be overridden with `[JsonRpcParam("name")]`.
113145

114-
Every method lives in a *session*, a named set of methods. Everything above goes into the default session (`Handler.DefaultSessionId()`), which is all most applications need. Overloads that take a `sessionId` let one process serve separate method sets; see [Sessions and context](#sessions-and-context).
146+
Every method lives in a *session*, a named set of methods. Both examples register methods in the default session (`Handler.DefaultSessionId()`), which is all most applications need. Lambdas and classes can be mixed in one session, but method names must be unique: `BindMethod` throws for a name that is already registered, and a class bound afterwards replaces an earlier registration of the same name. Both examples register `add`, so keep one of them. Overloads that take a `sessionId` let one process serve separate method sets; see [Sessions and context](#sessions-and-context).
147+
148+
The next step drives `CalculatorService` in process, without a transport.
115149

116150
### 2. Process requests
117151

@@ -124,23 +158,22 @@ using AustinHarris.JsonRpc;
124158
var service = new CalculatorService(); // binds itself to the default session; keep a reference
125159
126160
// Strings, asynchronous invocation.
127-
string response = await JsonRpcProcessor.ProcessAsync("{\"jsonrpc\":\"2.0\",\"method\":\"add\",\"params\":[1,2],\"id\":1}");
161+
string response = await JsonRpcProcessor.ProcessAsync("""{"jsonrpc":"2.0","method":"add","params":[1,2],"id":1}""");
128162
// {"jsonrpc":"2.0","result":3.0,"id":1}
129163
130164
// Strings, synchronous, on the calling thread. Named parameters.
131-
string sync = JsonRpcProcessor.ProcessSync("{\"method\":\"multiply\",\"params\":{\"l\":6,\"r\":7},\"id\":2}");
165+
string sync = JsonRpcProcessor.ProcessSync("""{"method":"multiply","params":{"l":6,"r":7},"id":2}""");
132166
// {"jsonrpc":"2.0","result":42,"id":2}
133167
134168
// Bytes: the native path. The string overloads transcode into it.
135-
byte[] request = Encoding.UTF8.GetBytes("{\"method\":\"add\",\"params\":[2,3],\"id\":3}");
136169
var output = new ArrayBufferWriter<byte>();
137-
JsonRpcProcessor.Process(Handler.DefaultSessionId(), request.AsSpan(), output);
170+
JsonRpcProcessor.Process(Handler.DefaultSessionId(), """{"method":"add","params":[2,3],"id":3}"""u8, output);
138171
Console.WriteLine(Encoding.UTF8.GetString(output.WrittenSpan)); // nothing is written for a notification
139172
```
140173

141-
Batches (`[{...},{...}]`) and notifications (requests without an `id`) are handled per the spec: a batch answers with an array, a notification produces nothing. The byte overloads take `ReadOnlySpan<byte>`, `ReadOnlyMemory<byte>` or `ReadOnlySequence<byte>`; pass a `byte[]` as `AsSpan()`, because on C# 12 a bare array is ambiguous between the memory and span overloads.
174+
Batches (`[{...},{...}]`) and notifications (requests without an `id`) are handled per the spec: a batch answers with an array, a notification produces nothing. The byte overloads take `ReadOnlySpan<byte>`, `ReadOnlyMemory<byte>` or `ReadOnlySequence<byte>`. A `"""..."""u8` literal is a `ReadOnlySpan<byte>` (C# 11 and later). A bare `byte[]` selects the span overload on C# 14 and later; on C# 12 and 13 it is ambiguous between the memory and span overloads, so pass it as `AsSpan()` there.
142175

143-
That is the whole server. The rest of this page is about exposing methods, putting a transport in front, and what happens when things go wrong.
176+
That is the whole in-process server. The rest of this page is about exposing methods, putting a transport in front, and what happens when things go wrong.
144177

145178
## Defining methods
146179

0 commit comments

Comments
 (0)