Chio/Docs

JVM and .NET HTTP Frameworks

Two managed-runtime examples that govern the same GET /hello / POST /echo contract through a local Chio sidecar. Spring Boot uses a servlet filter; ASP.NET Core uses a middleware in the standard pipeline.

What it shows

  • io.backbay.chio:chio-spring-boot: a ChioFilter registered as a FilterRegistrationBean at highest precedence.
  • Backbay.Chio for ASP.NET Core: AddChioProtection() plus UseChioProtection().
  • Both bind to a local chio api protect sidecar over HTTP. No FFI, no embedded kernel.
  • Request bodies remain readable by the controller after Chio hashing. Receipt ids appear on the response header path for governed routes.

Binding pattern

The JVM and .NET SDKs do the same thing the Node and Python SDKs do: speak HTTP to a sidecar that holds the kernel, the policy, and the receipt store. There is no JNI/PInvoke step. That keeps the runtime footprint small and avoids platform-specific native loading. See HTTP Framework Middleware.

Prerequisites

  • Spring Boot: a JDK 17+ (the example uses Kotlin 2.3 + Spring Boot 3.2). The Gradle wrapper at sdks/jvm/gradlew handles the rest.
  • .NET: the .NET 8 SDK. The example pulls ChioMiddleware as a project reference.
  • The chio CLI on PATH. The smokes start a local chio trust serve and chio api protect.

Run them

bash
cd examples/hello-spring-boot   # or hello-dotnet
./run.sh

# Full smoke (sidecar + trust + deny + allow)
./smoke.sh

Default ports:

ExampleEnv varDefault
hello-spring-bootSpring Boot default config8080
hello-dotnetHELLO_DOTNET_PORT8019

Spring Boot

The integration is one bean. Build a ChioFilter with the sidecar URL, wrap it in a FilterRegistrationBean, and give it Ordered.HIGHEST_PRECEDENCE so it runs before any business filter.

Build file

examples/hello-spring-boot/build.gradle.kts
dependencies {
    implementation(platform("org.springframework.boot:spring-boot-dependencies:3.2.2"))
    implementation("io.backbay.chio:chio-spring-boot:0.1.0")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
}

Application

examples/hello-spring-boot/src/main/kotlin/example/hello/HelloSpringBootApplication.kt
package example.hello

import io.backbay.chio.ChioFilter
import io.backbay.chio.ChioFilterConfig
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.boot.web.servlet.FilterRegistrationBean
import org.springframework.context.annotation.Bean
import org.springframework.core.Ordered
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController

@SpringBootApplication
class HelloSpringBootApplication {
    @Bean
    fun chioFilterRegistration(): FilterRegistrationBean<ChioFilter> {
        val filter = ChioFilter(
            ChioFilterConfig(
                sidecarUrl = System.getenv("CHIO_SIDECAR_URL") ?: "http://127.0.0.1:9090",
            ),
        )
        return FilterRegistrationBean<ChioFilter>().apply {
            setFilter(filter)
            addUrlPatterns("/*")
            order = Ordered.HIGHEST_PRECEDENCE
        }
    }
}

@RestController
class HelloController {
    @GetMapping("/healthz")
    fun healthz(): Map<String, String> = mapOf("status" to "ok")

    @GetMapping("/hello")
    fun hello(): Map<String, String> = mapOf("message" to "hello from spring-boot")

    @PostMapping("/echo", consumes = [MediaType.APPLICATION_JSON_VALUE])
    fun echo(@RequestBody payload: EchoRequest): Map<String, Any> = mapOf(
        "message" to payload.message,
        "count" to payload.count,
    )
}

data class EchoRequest(val message: String, val count: Int = 1)

fun main(args: Array<String>) {
    runApplication<HelloSpringBootApplication>(*args)
}

Notes: ChioFilter wraps the servlet request to cache the body bytes for replay; the @RequestBody binding still works on echo after the filter has hashed the bytes. The receipt id is set on the response header path; controllers do not need to thread it through manually.


ASP.NET Core

Two lines: register the service, add the middleware. The example uses ASP.NET's minimal API style.

Project file

examples/hello-dotnet/HelloChio.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="../../sdks/dotnet/ChioMiddleware/src/ChioMiddleware.csproj" />
  </ItemGroup>

</Project>

Program

examples/hello-dotnet/Program.cs
using Backbay.Chio;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddChioProtection();

var app = builder.Build();
app.UseChioProtection();

app.MapGet("/healthz", () => Results.Json(new { status = "ok" }));

app.MapGet("/hello", () => Results.Json(new { message = "hello from dotnet" }));

app.MapPost("/echo", (EchoRequest payload) =>
    Results.Json(new
    {
        message = payload.Message,
        count = payload.Count,
    }));

app.Run();

internal sealed record EchoRequest(string Message, int Count = 1);

Notes: UseChioProtection() installs the middleware near the start of the pipeline. Body bytes are buffered with EnableBuffering()-style semantics so the model binder can re-read them. Configure the sidecar URL via standard ASP.NET configuration (env var, appsettings, or DI options).

bash
./run.sh
# starts dotnet on http://127.0.0.1:8019 (override with HELLO_DOTNET_PORT)

Critical wiring

One bean (Spring) and one middleware call (.NET). These are the lines that flip on Chio enforcement.

examples/hello-spring-boot/.../HelloSpringBootApplication.kt:151
@Bean
fun chioFilterRegistration(): FilterRegistrationBean<ChioFilter> {
    val filter = ChioFilter(ChioFilterConfig(sidecarUrl = ...))
    return FilterRegistrationBean<ChioFilter>().apply {
        setFilter(filter)
        addUrlPatterns("/*")
        order = Ordered.HIGHEST_PRECEDENCE
    }
}
examples/hello-dotnet/Program.cs:4
builder.Services.AddChioProtection();
var app = builder.Build();
app.UseChioProtection();

Smoke assertions

Both smokes drive the same three-call pattern: GET allow, POST deny without capability, POST allow with capability. The deny body for Spring and .NET both carry error: chio_access_denied and a receipt_id field.

examples/hello-spring-boot/smoke.sh
# /hello
assert body["message"] == "hello from spring-boot", body

# /echo without token (403)
assert body["error"] == "chio_access_denied", body
assert body["receipt_id"], body

# /echo with X-Chio-Capability
assert body["message"] == "hello", body
assert body["count"] == 2, body
examples/hello-dotnet/smoke.sh
# Same shape: chio_access_denied + receipt_id on deny
assert body["message"] == "hello from dotnet", body
assert body["error"] == "chio_access_denied", body
assert body["receipt_id"], body

Inspect after

bash
cd .artifacts/$(ls -t .artifacts | head -1)

# Receipt id arrives as x-chio-receipt-id (lower-case)
grep -i x-chio-receipt-id hello.headers deny.headers allow.headers

# 3 persisted receipts in the sidecar SQLite store
wc -l receipts.ndjson                   # expect: 3
jq -r '.verdict.outcome' receipts.ndjson | sort | uniq -c
# expect: 2 allow, 1 deny

# Confirm the deny receipt id matches the one in deny.json
jq -r '.receipt_id' deny.json
grep -i x-chio-receipt-id deny.headers

# Direct SQLite peek
sqlite3 state/sidecar-receipts.sqlite3 \
  "select id, route_method, route_path from receipts order by created_at;"

When this fits

Use this when: your Spring Boot or ASP.NET service can take an SDK dependency and you want receipts available on the response header path with body bytes replayed for the controller. Don't use this if the service is closed-source or you cannot redeploy: run chio api protect in front of it instead. See OpenAPI Sidecar.

JVM vs .NET, in one table

AspectSpring BootASP.NET Core
SDK packageio.backbay.chio:chio-spring-bootBackbay.Chio (project reference)
Pipeline shapeServlet filter via FilterRegistrationBeanASP.NET middleware
OrderOrdered.HIGHEST_PRECEDENCEOrder of UseChioProtection() in Program.cs
Body reuseWrapped servlet requestStream buffering on HttpRequest
Sidecar URLCHIO_SIDECAR_URL envASP.NET config (env, appsettings)

Why HTTP and not FFI

Both runtimes have mature FFI surfaces (JNI, P/Invoke), but the sidecar shape wins on three counts:

  • No native build per platform: the Java archive and the .NET assembly stay pure managed code; the sidecar binary handles platform-specific concerns.
  • Single source of truth: the sidecar holds policy, kernel state, and receipts. Multiple managed processes can share one sidecar.
  • Crash isolation: a bug in the kernel cannot bring down the JVM or the .NET host.

The cost is one localhost roundtrip per evaluated request, which the sidecar amortizes well in practice.


Next

JVM and .NET HTTP Frameworks · Chio Docs