From f1593c02767a74dd915f666f6c603a50ba4ea35d Mon Sep 17 00:00:00 2001 From: Austin Harris Date: Thu, 24 Sep 2026 22:28:36 -0600 Subject: [PATCH] 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. --- Json-Rpc/README.md | 52 ++++++++++++++++++++++++++++++++++++++++------ README.md | 49 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 87 insertions(+), 14 deletions(-) diff --git a/Json-Rpc/README.md b/Json-Rpc/README.md index 69b1037..8212521 100644 --- a/Json-Rpc/README.md +++ b/Json-Rpc/README.md @@ -26,7 +26,42 @@ Coming from 1.x? Read [What is new in 2.0](https://astn.github.io/JSON-RPC.NET/c ### Declare a service -Create `CalculatorService.cs` with the service below. +Save this as `server.cs`, 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 lambdas served by Kestrel at `/rpc`. + +```csharp +#:sdk Microsoft.NET.Sdk.Web +#:package AustinHarris.JsonRpc.AspNetCore@2.0.0-preview.1 + +using AustinHarris.JsonRpc; +using AustinHarris.JsonRpc.AspNetCore; + +ServiceBinder.BindMethod("add", (double l, double r) => l + r); +ServiceBinder.BindMethod("greet", (string who) => "hello " + who); + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddJsonRpc(); + +var app = builder.Build(); +app.MapJsonRpc("/rpc"); +app.Run(); +``` + +Run `dotnet run server.cs`; Kestrel prints its listening URL. +Use `dotnet run server.cs -- --urls http://127.0.0.1:5077` to pin it for this request from another terminal: + +```bash +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}' +``` + +```json +{"jsonrpc":"2.0","result":3.0,"id":1} +``` + +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. + +For a service class, create `CalculatorService.cs`. Derive from `JsonRpcService` and mark exposed methods with `[JsonRpcMethod]`. Constructing the service registers its methods in the default session. @@ -47,8 +82,13 @@ public class CalculatorService : JsonRpcService ``` Methods can be `private`. Parameters can be positional or named. +Optional parameter defaults are honoured; `[JsonRpcParam("name")]` overrides a parameter's JSON name. Keep the service instance alive; it serves concurrent requests, so its state must be thread-safe. +Both examples use the default session (`Handler.DefaultSessionId()`). +Lambdas and classes can be mixed in one session when their method names differ; both examples register `add`, so keep one of them. +The next step drives `CalculatorService` in process, without a transport. + ### Process requests Put this code in `Program.cs` in a console project targeting `net8.0` or `net10.0`. @@ -64,17 +104,16 @@ using AustinHarris.JsonRpc; var service = new CalculatorService(); // binds itself to the default session; keep a reference // Strings, asynchronous invocation. -string response = await JsonRpcProcessor.ProcessAsync("{\"jsonrpc\":\"2.0\",\"method\":\"add\",\"params\":[1,2],\"id\":1}"); +string response = await JsonRpcProcessor.ProcessAsync("""{"jsonrpc":"2.0","method":"add","params":[1,2],"id":1}"""); // {"jsonrpc":"2.0","result":3.0,"id":1} // Strings, synchronous, on the calling thread. Named parameters. -string sync = JsonRpcProcessor.ProcessSync("{\"method\":\"multiply\",\"params\":{\"l\":6,\"r\":7},\"id\":2}"); +string sync = JsonRpcProcessor.ProcessSync("""{"method":"multiply","params":{"l":6,"r":7},"id":2}"""); // {"jsonrpc":"2.0","result":42,"id":2} // Bytes: the native path. The string overloads transcode into it. -byte[] request = Encoding.UTF8.GetBytes("{\"method\":\"add\",\"params\":[2,3],\"id\":3}"); var output = new ArrayBufferWriter(); -JsonRpcProcessor.Process(Handler.DefaultSessionId(), request.AsSpan(), output); +JsonRpcProcessor.Process(Handler.DefaultSessionId(), """{"method":"add","params":[2,3],"id":3}"""u8, output); Console.WriteLine(Encoding.UTF8.GetString(output.WrittenSpan)); // nothing is written for a notification ``` @@ -86,7 +125,8 @@ The string responses appear in the comments above. The byte call prints: A batch returns an array when it contains calls that need responses. A notification has no `id` and produces no response. -Pass a `byte[]` as `AsSpan()` to select the span overload. +A `"""..."""u8` literal is a `ReadOnlySpan` (C# 11 and later). +A bare `byte[]` is ambiguous between the memory and span overloads on C# 12 and 13; pass it as `AsSpan()` there. Use `JsonRpcProcessor.ProcessAsync` for methods returning `Task` or `ValueTask`. The synchronous entry points do not await those methods. diff --git a/README.md b/README.md index 40a9a95..526a071 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,39 @@ Add `AustinHarris.JsonRpc.Newtonsoft` or `AustinHarris.JsonRpc.SystemTextJson` i ### 1. Declare a service -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. +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`. + +```csharp +#:sdk Microsoft.NET.Sdk.Web +#:package AustinHarris.JsonRpc.AspNetCore@2.0.0-preview.1 + +using AustinHarris.JsonRpc; +using AustinHarris.JsonRpc.AspNetCore; + +ServiceBinder.BindMethod("add", (double l, double r) => l + r); +ServiceBinder.BindMethod("greet", (string who) => "hello " + who); + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddJsonRpc(); + +var app = builder.Build(); +app.MapJsonRpc("/rpc"); +app.Run(); +``` + +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: + +```bash +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}' +``` + +```json +{"jsonrpc":"2.0","result":3.0,"id":1} +``` + +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. + +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. ```csharp using AustinHarris.JsonRpc; @@ -111,7 +143,9 @@ public class CalculatorService : JsonRpcService 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")]`. -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). +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). + +The next step drives `CalculatorService` in process, without a transport. ### 2. Process requests @@ -124,23 +158,22 @@ using AustinHarris.JsonRpc; var service = new CalculatorService(); // binds itself to the default session; keep a reference // Strings, asynchronous invocation. -string response = await JsonRpcProcessor.ProcessAsync("{\"jsonrpc\":\"2.0\",\"method\":\"add\",\"params\":[1,2],\"id\":1}"); +string response = await JsonRpcProcessor.ProcessAsync("""{"jsonrpc":"2.0","method":"add","params":[1,2],"id":1}"""); // {"jsonrpc":"2.0","result":3.0,"id":1} // Strings, synchronous, on the calling thread. Named parameters. -string sync = JsonRpcProcessor.ProcessSync("{\"method\":\"multiply\",\"params\":{\"l\":6,\"r\":7},\"id\":2}"); +string sync = JsonRpcProcessor.ProcessSync("""{"method":"multiply","params":{"l":6,"r":7},"id":2}"""); // {"jsonrpc":"2.0","result":42,"id":2} // Bytes: the native path. The string overloads transcode into it. -byte[] request = Encoding.UTF8.GetBytes("{\"method\":\"add\",\"params\":[2,3],\"id\":3}"); var output = new ArrayBufferWriter(); -JsonRpcProcessor.Process(Handler.DefaultSessionId(), request.AsSpan(), output); +JsonRpcProcessor.Process(Handler.DefaultSessionId(), """{"method":"add","params":[2,3],"id":3}"""u8, output); Console.WriteLine(Encoding.UTF8.GetString(output.WrittenSpan)); // nothing is written for a notification ``` -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`, `ReadOnlyMemory` or `ReadOnlySequence`; pass a `byte[]` as `AsSpan()`, because on C# 12 a bare array is ambiguous between the memory and span overloads. +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`, `ReadOnlyMemory` or `ReadOnlySequence`. A `"""..."""u8` literal is a `ReadOnlySpan` (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. -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. +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. ## Defining methods