Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3fa2d7c
feat: add SDK foundation and fix the build under the .NET 10 SDK
OmarAlJarrah Jun 15, 2026
7ac845b
docs: add .NET SDK platform architecture and build plan
OmarAlJarrah Jun 14, 2026
3173e43
docs: add serde + System.Text.Json slice design
OmarAlJarrah Jun 14, 2026
454e98e
docs: add options slice design; narrow Core deps to logging + diagnos…
OmarAlJarrah Jun 14, 2026
1858db9
docs: add instrumentation + context chain slice design
OmarAlJarrah Jun 14, 2026
5d7af42
docs: add pipeline spine slice design
OmarAlJarrah Jun 15, 2026
257008d
docs: add core policies slice design
OmarAlJarrah Jun 15, 2026
a2a3b42
docs: add auth slice design
OmarAlJarrah Jun 15, 2026
9be6d7e
docs: add pagination, SSE, webhooks, and DI integration slice designs
OmarAlJarrah Jun 15, 2026
480b045
docs: add serde + System.Text.Json implementation plan
OmarAlJarrah Jun 15, 2026
e348a05
chore: scaffold Dexpace.Sdk.Serialization.SystemTextJson project
OmarAlJarrah Jun 15, 2026
3b7f9ae
build: run the test suite on both net8.0 and net10.0
OmarAlJarrah Jun 15, 2026
280ece8
fix: require an http or https scheme for request URLs
OmarAlJarrah Jun 15, 2026
23603dc
feat: add ISerde serialization seam to core
OmarAlJarrah Jun 15, 2026
32f1841
feat: add SystemTextJsonSerde with async serialize/deserialize
OmarAlJarrah Jun 15, 2026
0585aba
feat: add synchronous serialize/deserialize to SystemTextJsonSerde
OmarAlJarrah Jun 15, 2026
f9a2f96
feat: map System.Text.Json failures to SDK serialization exceptions
OmarAlJarrah Jun 15, 2026
325daf4
fix: map System.Text.Json runtime failures consistently across sync a…
OmarAlJarrah Jun 15, 2026
d617f49
feat: add RequestBody.FromValue serde convenience
OmarAlJarrah Jun 15, 2026
28914d7
feat: add ResponseBody.ReadValueAsync serde convenience
OmarAlJarrah Jun 15, 2026
713cdf0
feat: add HttpResponseException.GetErrorAsync typed-error accessor
OmarAlJarrah Jun 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
root = true

# Copyright (c) 2026 dexpace and Omar Aljarrah.
# Licensed under the MIT License. See LICENSE in the repository root for details.
#
# Single source of truth for formatting and analyzer severities. The build runs with
# TreatWarningsAsErrors and AnalysisLevel=latest-recommended (see Directory.Build.props), so any
# rule left at warning/error must be satisfied by the code. Rules dialled down below are
# intentional design choices, each annotated with its rationale.

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space

[*.{cs,csx}]
indent_size = 4
max_line_length = 120

# Prefer modern C# idioms (these are the conventions the code already follows).
csharp_style_namespace_declarations = file_scoped:warning
csharp_prefer_braces = true:warning
csharp_using_directive_placement = outside_namespace:warning
dotnet_sort_system_directives_first = true
csharp_style_var_for_built_in_types = false:suggestion

# --- Intentional analyzer exemptions ---------------------------------------------------------

# HTTP field names, media-type tokens, and protocol identifiers have a canonical *lower-case*
# form (RFC 7230 §3.2). Lowercasing is correct here; ToUpperInvariant would be wrong.
dotnet_diagnostic.CA1308.severity = none

# The SDK deliberately accepts string URLs at its ergonomic entry points (e.g. Request.Get).
# A System.Uri overload is also provided for callers that prefer it.
dotnet_diagnostic.CA1054.severity = none
dotnet_diagnostic.CA1055.severity = none
dotnet_diagnostic.CA1056.severity = none

# Argument null-checks are applied where they matter via ArgumentNullException.ThrowIfNull;
# CA1062's blanket requirement on every public parameter is noise for this surface.
dotnet_diagnostic.CA1062.severity = none

# ConfigureAwait(false) is applied in library code where it matters, but `await using` / `await
# foreach` emit implicit awaits the rule cannot see; the SDK does not depend on capturing context.
dotnet_diagnostic.CA2007.severity = none

[tests/**/*.cs]
# Test method names use Pascal_Snake casing by convention (e.g. Method_DoesThing_WhenCondition).
dotnet_diagnostic.CA1707.severity = none
45 changes: 45 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

jobs:
build:
name: build & test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
# The .NET 8 runtime is needed to execute the net8.0 test target; the pinned
# SDK (net10) comes from global.json and drives the build.
dotnet-version: 8.0.x
global-json-file: global.json

- name: Restore
run: dotnet restore

# TreatWarningsAsErrors + AnalysisLevel=latest-recommended make the build itself the lint gate.
- name: Build
run: dotnet build --configuration Release --no-restore

- name: Test
run: >-
dotnet test --configuration Release --no-build
--collect:"XPlat Code Coverage"
--logger "trx;LogFileName=test-results.trx"

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: "**/test-results.trx"
30 changes: 30 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Build output
[Bb]in/
[Oo]bj/
[Oo]ut/
artifacts/

# .NET / Rider / VS
.vs/
*.user
*.suo
*.userprefs
.idea/
*.DotSettings.user

# Test / coverage
[Tt]est[Rr]esults/
coverage/
*.coverage
*.trx
*.cobertura.xml

# NuGet
*.nupkg
*.snupkg
.nuget/
project.lock.json

# OS
.DS_Store
Thumbs.db
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Changelog

All notable changes to this project are documented here. The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Initial repository structure: `Dexpace.Sdk.sln`, central build/package configuration
(`Directory.Build.props`, `Directory.Packages.props`, `.editorconfig`, `global.json`),
`.gitignore`, and a GitHub Actions CI workflow (build + test).
- `Dexpace.Sdk.Core` foundation slice:
- `Http/Common`: `Method`, `Protocol` (+ wire-form conversions), `MediaType` (+ `CommonMediaTypes`),
`HttpHeaderName` (+ well-known names), and the immutable case-insensitive `Headers` multimap.
- `Http/Request`: `Request` and the `RequestBody` abstraction (bytes / string / stream factories,
replayability).
- `Http/Response`: `Response`, the `ResponseBody` abstraction, and `Status` (+ well-known codes).
- `Client`: `IHttpClient` / `IAsyncHttpClient` transport SPIs and sync/async bridges.
- `Errors`: the `SdkException` hierarchy.
- `Dexpace.Sdk.Http.SystemNet`: reference transport adapting `System.Net.Http.HttpClient` to the SPI.
- `Dexpace.Sdk.Core.Tests`: xUnit coverage for media types, headers, methods, statuses, bodies,
request building, and the transport.

[Unreleased]: https://github.com/dexpace/dotnet-sdk/commits/main
133 changes: 133 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Repository

The .NET counterpart to [`dexpace/java-sdk`](https://github.com/dexpace/java-sdk) and
[`dexpace/python-sdk`](https://github.com/dexpace/python-sdk). The architecture follows the same
shape (immutable HTTP models, transport SPI, body abstractions, typed errors) but the public API
uses .NET idioms — `record` / `readonly record struct` instead of builder objects, `interface`
instead of Kotlin `fun interface` / Python `Protocol`, `IDisposable` / `IAsyncDisposable` instead
of `AutoCloseable` / context managers, `Task<T>` as the async contract. The pluggable I/O seam that
exists in the Java SDK (`IoProvider` over Okio) was intentionally **not** ported: .NET's
`System.IO.Stream`, `Memory<byte>`, and `IAsyncDisposable` cover the same surface natively, exactly
as the Python port leans on `bytes` / `BinaryIO`.

## Build & test (from the repository root)

```bash
dotnet restore
dotnet build --configuration Release # the build IS the lint gate (warnings-as-errors)
dotnet test --configuration Release
dotnet format --verify-no-changes # formatting gate (uses .editorconfig)
```

The .NET SDK is pinned in `global.json` (10.0.100, `rollForward: latestFeature`). Library projects
target `net8.0`.

## Conventions (enforced — match these when adding code)

- **net8.0 libraries, C# `latest`, `Nullable` + `ImplicitUsings` enabled.** Modern idioms: file-scoped
namespaces, records, `readonly record struct`, pattern matching, `init` accessors, collection
expressions where they fit.
- **`TreatWarningsAsErrors` + `AnalysisLevel=latest-recommended` + `EnforceCodeStyleInBuild`.** The
build is the lint gate. Rule severities live in `.editorconfig`; a handful of analyzer rules are
deliberately dialled down there with a documented rationale (CA1308 lower-casing, CA1054/55/56
string URLs, CA1062, CA2007) — do not silence others without justification.
- **Immutable models.** `record` / `readonly record struct`; mutate via `with` expressions or `With*`
helpers. No builder-as-object types — object initializers and `with` make them redundant. `Headers`
is the one mutable-builder exception (`Headers.Builder`) for batched edits.
- **Interfaces for SPIs.** `IHttpClient`, `IAsyncHttpClient` are the transport seams.
`Dexpace.Sdk.Core` ships **no** transport; transports adapt one HTTP library each and live in their
own project (`Dexpace.Sdk.Http.*`).
- **Deterministic cleanup.** `Response`, `ResponseBody`, and transports implement `IDisposable` /
`IAsyncDisposable`. Single-use bodies (stream-backed) throw `StreamConsumedException` on a second
read; call `RequestBody.ToReplayableAsync()` before the first send if retries are needed.
- **No runtime dependencies in `core`.** It builds against the BCL only. `SourceLink` is the only
build-time package. Transports may depend on their HTTP library; `core` may not.
- **Narrow, fully-documented public API.** `GenerateDocumentationFile` is on, so every public member
needs a `///` XML doc comment (missing docs are CS1591 → build error). Implementation helpers are
`internal` (with `InternalsVisibleTo` for the test and transport assemblies).
- **Central package versions.** `Directory.Packages.props` is the single source of truth (the
`libs.versions.toml` analog). `PackageReference`s carry no `Version` attribute.
- **MIT license header on every `.cs` file** — the two-line block, src and tests alike:

```csharp
// Copyright (c) 2026 dexpace and Omar Aljarrah.
// Licensed under the MIT License. See LICENSE in the repository root for details.
```

- **Commit style:** `chore:` for refactors/cleanup; `feat:` for new features; `fix:` for bug fixes;
`docs:` for documentation-only changes.

## Repository Layout

A single solution (`Dexpace.Sdk.sln`) with central build/package configuration at the root. Each
distribution is its own project under `src/`; tests under `tests/`.

```
dotnet-sdk/
├── Dexpace.Sdk.sln
├── Directory.Build.props # shared compiler + package metadata
├── Directory.Packages.props # central package versions
├── .editorconfig # formatting + analyzer severities
├── global.json # pinned .NET SDK
├── nuget.config
├── docs/architecture.md
└── src/
├── Dexpace.Sdk.Core/ # toolkit; no transport, BCL-only
│ ├── Http/Common/ # Method, Protocol, MediaType, CommonMediaTypes,
│ │ # HttpHeaderName, Headers
│ ├── Http/Request/ # Request, RequestBody
│ ├── Http/Response/ # Response, ResponseBody, Status
│ ├── Client/ # IHttpClient, IAsyncHttpClient, HttpClientExtensions
│ └── Errors/ # SdkException + ServiceRequest/Response, HttpResponse,
│ # streaming, serialization, pipeline exceptions
└── Dexpace.Sdk.Http.SystemNet/ # reference transport over System.Net.Http.HttpClient
└── tests/
└── Dexpace.Sdk.Core.Tests/ # xUnit suite (references core + transport)
```

## Architecture — Big Picture

The SDK is an **HTTP-client toolkit, not an HTTP client**. `Dexpace.Sdk.Core` provides abstractions,
models, and (over time) pipelines; consuming libraries plug in a concrete transport via
`IHttpClient` / `IAsyncHttpClient`.

Layered, bottom-up:

1. **Bodies** — `RequestBody.WriteToAsync(Stream)` is the outgoing streaming surface;
`ResponseBody.OpenReadAsync` / `ReadAsBytesAsync` / `ReadAsStringAsync` drain the incoming side.
Bytes/string bodies are replayable; stream bodies are single-use.
2. **HTTP value models** (`Http/Common`, `Http/Response/Status`) — immutable, case-insensitive
`Headers` multimap; `MediaType` with quote-aware parse/round-trip; `Method`, `Protocol`, `Status`
value types with well-known instances.
3. **Request / Response** — `Request` is an immutable `record` (absolute `Uri`); `Response` is a
disposable carrier of status/headers/body/protocol.
4. **Transport SPI** (`Client`) — async-first `IAsyncHttpClient` plus a synchronous `IHttpClient`,
with `AsAsync` / `AsBlocking` bridges.
5. **Errors** — `SdkException` roots the hierarchy: `ServiceRequestException` (never sent, retry-safe
on idempotent methods), `ServiceResponseException` (sent, response unreadable),
`HttpResponseException` (4xx/5xx received intact), plus lifecycle/serialization/pipeline failures.

## Things That Will Bite You

- **The build is the lint gate.** A missing `///` doc comment on a public member, an unused `using`,
or an unsuppressed analyzer finding fails the build (`TreatWarningsAsErrors`). Build before
declaring done.
- **`Dexpace.Sdk.Core` must stay BCL-only.** Do not add a runtime `PackageReference` to it — model
third-party needs behind an interface and implement them in an adapter project.
- **Single-use bodies throw on second consumption.** `RequestBody.FromStream` /
`ResponseBody.FromStream` raise `StreamConsumedException` the second time. Buffer first
(`ToReplayableAsync`) when retries are in play.
- **Transports are ownership-aware.** A caller-supplied `System.Net.Http.HttpClient` is never disposed
by `SystemNetHttpClient`; only an internally created one is.
- **Central Package Management is on.** Add new dependency versions to `Directory.Packages.props`, and
reference them without a `Version` attribute.

## Planned (not yet implemented)

Mirroring the Java/Python ports: pipeline (staged policies — redirect, retry, idempotency, set-date,
client-identity, logging, tracing), context promotion chain, auth (token credentials, bearer/basic,
RFC 7235 challenges), SSE, pagination, webhooks, and instrumentation. See `docs/architecture.md`.
Loading
Loading