Vouchfx.Community.JsonRpc 1.0.0-alpha.1

Prefix Reserved
This is a prerelease version of Vouchfx.Community.JsonRpc.
dotnet add package Vouchfx.Community.JsonRpc --version 1.0.0-alpha.1
                    
NuGet\Install-Package Vouchfx.Community.JsonRpc -Version 1.0.0-alpha.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Vouchfx.Community.JsonRpc" Version="1.0.0-alpha.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Vouchfx.Community.JsonRpc" Version="1.0.0-alpha.1" />
                    
Directory.Packages.props
<PackageReference Include="Vouchfx.Community.JsonRpc" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Vouchfx.Community.JsonRpc --version 1.0.0-alpha.1
                    
#r "nuget: Vouchfx.Community.JsonRpc, 1.0.0-alpha.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Vouchfx.Community.JsonRpc@1.0.0-alpha.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Vouchfx.Community.JsonRpc&version=1.0.0-alpha.1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Vouchfx.Community.JsonRpc&version=1.0.0-alpha.1&prerelease
                    
Install as a Cake Tool

rpc.json-rpc — JSON-RPC 2.0 over HTTP

The first Community-tier provider for the vouchfx hub — listed in the community provider registry, hosted in this repository, and richer than the copyable template/ skeleton (though it does not carry the Vouched badge). It doubles as the reference implementation the implementing-a-provider guide walks through: a real HTTP-calling, BCL-plus-JsonPath.Net provider built outside the engine repo, following exactly the same patterns the engine's own Core http.rest and mail-expect.smtp providers use.

What it is

JSON-RPC 2.0 is a small, transport-agnostic remote-procedure-call wire protocol: a JSON request envelope naming a method and params, and a JSON response envelope carrying either a result or an error. It is the wire protocol behind:

  • the Language Server Protocol (every editor ↔ language-server exchange),
  • Ethereum JSON-RPC APIs (eth_call, eth_getBalance, …),
  • Bitcoin Core's RPC interface,
  • and many internal microservice RPC layers.

This provider issues JSON-RPC 2.0 requests over HTTP and asserts on the response — letting an .e2e.yaml suite exercise a JSON-RPC endpoint the same way http.rest exercises a plain REST one.

Worked examples

1. Happy path, with a capture

metadata:
  name: jsonrpc-sum-happy-path

environment:
  services:
    calc:
      image: my-org/jsonrpc-calculator:latest

steps:
  - id: call-sum
    type: rpc.json-rpc
    url: "http://{calc-host}:{calc-port}/rpc"
    method: sum
    params:
      a: 2
      b: 3
    expect:
      result:
        - path: "$.sum"
          value: 5
    capture:
      total: "$.result.sum"

params is a YAML mapping, so it becomes a JSON-RPC named-params object ({"a":2,"b":3}). expect.result runs a JSONPath assertion against the response envelope's result value. capture (the engine-standard field, DSL §6.1) reads the full envelope — hence "$.result.sum", not "$.sum" — and writes total into Vars for a later step.

url, method, and every string leaf of params are resolved at step-execution time through the same {placeholder} + ${secret:source/path} mechanism (Secret_Helpers.ResolveTemplate, §17) — see examples 3 and 4 below, where params carries a {orderId} placeholder. params is parsed into a JSON tree first and each string value is resolved individually, never by template-substituting the raw JSON text before parsing, so a resolved value can never corrupt the surrounding JSON structure.

2. Negative test — asserting a specific JSON-RPC error

steps:
  - id: call-unknown-method
    type: rpc.json-rpc
    url: "http://{calc-host}:{calc-port}/rpc"
    method: subtractWithCarry   # deliberately unsupported by the target service
    expect:
      error:
        code: -32601            # JSON-RPC 2.0 standard "Method not found"

expect.error.code is a negative test: the step passes only when the response genuinely is a JSON-RPC error envelope with exactly this code. expect.result and expect.error are mutually exclusive — declaring both fails model validation before any request is sent.

3. RETRY — polling an eventually-consistent read

steps:
  - id: wait-for-projection
    type: rpc.json-rpc
    url: "http://{projector-host}:{projector-port}/rpc"
    method: getOrderStatus
    params:
      orderId: "{orderId}"
    expect:
      result:
        - path: "$.status"
          value: "SHIPPED"
    verifyMode: RETRY
    timeout: PT10S

verifyMode: RETRY is a purely engine-side wrapper (CsxAssembler / Vouchfx.Engine.Abstractions.Retry.RetryRunner) that re-invokes this provider's emitted block, unchanged, until it passes or timeout elapses. The provider does not implement (and does not need) any "RETRY capability" interface — the only contract it honours is: write exactly one StepOutcome on every invocation, and use Fail (never Inconclusive) for a not-yet-satisfied assertion. The engine converts a sustained Fail into Inconclusive once the window elapses; see the table below.

The {orderId} inside params.orderId above is a genuine substitution, not a literal: it is resolved from Vars on every poll, exactly like {projector-host} in url.

4. Fire-and-forget notification

steps:
  - id: notify-audit-log
    type: rpc.json-rpc
    url: "http://{audit-host}:{audit-port}/rpc"
    method: recordEvent
    params: ["order.shipped", "{orderId}"]
    notification: true

params is a YAML sequence here, so it becomes a JSON-RPC positional params array (["order.shipped", "<orderId>"], with {orderId} resolved from Vars the same way as the mapping form above). notification: true omits the request id entirely; the step asserts only that the transport call succeeded (a 2xx HTTP status) and never parses a response envelope — there usually isn't one. notification is incompatible with expect (rejected at model validation) and with capture (there is no response body to capture from — see "Known limitations").

Verdict-mapping table

This is the teaching core of this provider — verified against the engine's own exception- to-verdict conventions (http.rest, mail-expect.smtp) rather than invented from scratch.

Condition Verdict Notes
Connection refused / DNS failure / TLS failure EnvironmentError Generic catch-all; a run-environment problem, not a product defect (§12.1).
Response body is not valid JSON EnvironmentError The server returned something that isn't even a JSON-RPC envelope.
Resolved url scheme is not http/https EnvironmentError A defence-in-depth guard, thrown before any request is sent.
${secret:source/path} resolves to nothing / unknown source EnvironmentError Reference-only observation (§17) — never the value, never even the exception message.
Client-side timeout (TaskCanceledException / TimeoutException) Inconclusive Mirrors http.rest: "the test could not complete due to a stall," not a defect.
expect.result declared, response is a JSON-RPC error instead Fail {"unexpectedError":true}
expect.result declared, response id does not match the request Fail {"idMismatch":true}
expect.result declared, any JSONPath assertion mismatches Fail {"resultMismatch":{"path":...,"expected":...,"actual":...}} — renderable as a diff, see below.
expect.result declared, all assertions match Pass
expect.error.code declared, response is a result envelope instead Fail {"unexpectedResult":true}
expect.error.code declared, response has neither result nor error Fail {"missingError":true}
expect.error.code declared, error present but code differs Fail {"errorCodeMismatch":{"expected":...,"actual":...}} — renderable as a diff.
expect.error.code declared, code matches Pass
Neither expect.result nor expect.error declared ("bare" call), an error envelope arrives Fail {"unexpectedError":true}
Bare call, id mismatch Fail {"idMismatch":true}
Bare call, clean success envelope with matching id Pass
notification: true, HTTP status is 2xx Pass No envelope is parsed.
notification: true, HTTP status is not 2xx Fail
capture: declared, JSONPath yields no match against the full envelope Inconclusive {"captureUnmet":"<varName>"} — mirrors http.rest's "upstream-capture-unmet" convention exactly; only evaluated when the primary assertion above did not already resolve to Fail.
notification: true and capture: both declared Fail Rejected at compile (Emit) time, not validation time — see "Known limitations". An author misconfiguration (a step shape that can never do what it declares), not a timing uncertainty, so it deliberately does not use Inconclusive (which would not break CI by default and could hide the mistake).
expect.result declared as an empty list (result: []) (rejected) Model-validation error, not a runtime verdict — an empty list would otherwise silently degrade to bare-call semantics; see "Known limitations".
params is present but not a mapping or a sequence (a scalar) (rejected) Schema and model-validation error — JSON-RPC 2.0 §4.2 requires params to be structured when present.
verifyMode: RETRY, the assertion never converges before timeout Inconclusive Written entirely by the engine's RetryRunner, not by this provider — a sustained Fail (or Inconclusive) is converted once the polling window elapses.

Two of the Fail shapes above — resultMismatch and errorCodeMismatch — implement the optional IStepDiffRenderer interface, so a failing assertion renders as a small expected-vs-observed table in supporting renderers, exactly as the Core mail-expect.smtp provider does for its own count mismatches.

Known limitations

  • HTTP transport only. No WebSocket, no stdio (the LSP transport most editors actually use), no raw TCP framing. Adding a transport is a separate provider (or a richer model with a transport: discriminator) — out of scope for this provider.
  • No batch requests. The JSON-RPC 2.0 spec allows a request array processed as a batch; this provider always sends exactly one request object per step.
  • No headers field. Unlike http.rest, this provider's model has no way to set arbitrary request headers (e.g. Authorization). The engine's canonical pattern for a header value is Secret_Helpers.ResolveTemplate inside the emitted helper's guarded region (exactly as this provider already does for url, method, and every string leaf of params) — adding headers would cost only a small, mechanical amount of extra Bind/Emit plumbing (parallel name/value-template arrays, mirroring http.rest's own header handling almost verbatim). It is omitted here to keep the teaching surface focused on the JSON-RPC envelope semantics themselves.
  • url, method, and params all support ${secret:...}, even though the brief for this provider only asked for {placeholder} support — Secret_Helpers.ResolveTemplate gives both in a single call, at no extra cost, so it was included throughout. params is parsed into a JSON tree and each STRING LEAF is resolved individually (never by template-substituting the raw JSON text before parsing), so a resolved value can never corrupt the surrounding JSON structure, even if it contains a quote or a brace.
  • Captures are always evaluated as JSONPath, regardless of an explicit {xpath: ...} capture form — a JSON-RPC envelope is never XML, so an XPath-typed capture is simply passed through as raw expression text (it will normally just fail to match, yielding Inconclusive, not a hard validation error).
  • notification: true + expect is rejected at model validation (a clear, author-facing error). notification: true + capture cannot be rejected there — IProjectContext (available in Validate) carries no view of the step's capture map; only ICompileContext (available in Emit) does. That combination is instead short-circuited at Emit time to a trivial Fail block that skips the HTTP call entirely, with a {"error": "..."} observation explaining why. Fail, not Inconclusive: this is an author misconfiguration, not a timing uncertainty, and Inconclusive does not break CI by default (§12.1) — using it here would let the mistake hide.
  • expect.result must be non-empty when declared. Writing expect.result: [] is rejected at model validation rather than silently degrading to bare-call semantics — an author who writes an empty list almost certainly means "assert a result exists," which is a stronger claim than a bare call actually checks.
  • params must be structured. Per JSON-RPC 2.0 §4.2, params must be a mapping or a sequence when present; a bare scalar (params: 5, params: "x") is rejected both by the JSON Schema and by Validate.
  • Response id matching is string-only. This provider always sends the request id as a JSON string (the step id) and matches the response id with an ordinal string comparison. Some JSON-RPC ecosystems — Ethereum and Bitcoin Core's RPC APIs among them — conventionally use integer ids, and a server that coerces or echoes a numeric id back will fail the id-match check even though the call itself succeeded. There is no per-step way to opt into numeric ids in this provider.
  • No IResourceContributor. This provider declares no Aspire-managed infrastructure — the target is whatever absolute URL the author supplies, resolved entirely at execution time. There is consequently no dependency-reconciliation check against environment.services/environment.dependencies the way http.rest and mail-expect.smtp have.
  • This provider needs a JsonPath.Net PackageReference (pinned to 3.0.2, matching the engine's own Directory.Packages.props entry) beyond Vouchfx.Sdk — verified, not assumed: the engine's capture: field (and this provider's own expect.result) is evaluated with JSONPath.Net inside the provider's own emitted CSX, exactly as the Core http.rest and mq-expect.* providers do, which requires both the compile-time package reference (so ICompileReferenceContributor can name the type) and the contributor interface itself.
  • Conformance tests use a small custom harness (JsonRpcHarness), not only the published Vouchfx.Sdk.Testing.ProviderTestHarness. ProviderTestHarness does not run the ICompileReferenceContributor stage, and its default Roslyn reference set lacks System.Net.Http / System.Text.Json / JsonPath.Net entirely — a genuine HTTP-calling provider's execution tests cannot compile through it as-is. See Vouchfx.Community.JsonRpc.Tests/JsonRpcHarness.cs for the full explanation and the engine-repo precedent (HttpRestExecutionTests.cs) this mirrors. Tests that halt before any compile (schema/model-validation rejection) use the published harness directly, with no workaround needed.

Mapping to the community-hub path

This provider lives under community/, alongside template/. It is a real Community-tier provider — the first entry in registry/community-providers.json — hosted in the hub itself so the tier has a canonical, CI-tested reference implementation. It does not carry the Vouched badge: it has not been through the Vouched rubric. If you want to build on it as your own, independently-shipped provider:

  1. Copy Vouchfx.Community.JsonRpc to your own repository under your own namespace (never Vouchfx.Steps.* / Vouchfx.Engine.*).
  2. Publish it as a NuGet package under Apache-2.0.
  3. List it in the community provider index (Community tier), or work towards the Vouched badge rubric if you want platform-team endorsement — see CONTRIBUTING.md for both paths.
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0-alpha.1 119 7/10/2026