Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ name: CI
# Transport conformance kit (Dexpace.Sdk.Conformance, phase 8a): on. Two drivers run in the default test step, the reference
# transport (tests/Dexpace.Sdk.Http.SystemNet.Tests/SystemNetConformanceTests.cs, whose Owner = "8b" waivers phase 8b deletes
# as it closes their rows) and a test-only raw-socket client reached through DelegateHttpClient
# (tests/Dexpace.Sdk.Conformance.Tests/Drivers/); both carry [Trait("Category", "Conformance")] and run on every OS row. The kit
# is a measured library of the coverage gate, and a slice of it runs in the NativeAOT smoke consumer.
# (tests/Dexpace.Sdk.Conformance.Tests/Drivers/); both carry [Trait("Category", "Conformance")] and run on every OS row. Phase 9 adds a
# third, over the transport AddDexpaceClient builds from IHttpClientFactory
# (tests/Dexpace.Sdk.Extensions.DependencyInjection.Tests/Conformance/), which needs no CI step of its own: it runs in the solution's
# test step and its coverage report is picked up by name. The kit is a measured library of the coverage gate, and a slice of it runs
# in the NativeAOT smoke consumer, which also exercises the DI package (phase 9).
#
# Gates wired but inert until their subject exists, and the phase that turns each on:
# - SSE-37 architecture test (serde independence of SSE and paging): wired in
Expand Down
5 changes: 5 additions & 0 deletions BannedSymbols.txt
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,8 @@ P:System.Diagnostics.Activity.TraceIdGenerator;A library never sets the process-
# spec's bypass dialect (no NO_PROXY=* bypass, port defaulting, userinfo in the proxy URI, regex bypass entries). The transport
# installs a ProxyOptionsWebProxy from an explicit ProxyOptions, or goes direct (CFG-23, CFG-25, CFG-27, CFG-28, design §8.2, P8b-4).
P:System.Net.Http.HttpClient.DefaultProxy;The runtime's process-wide proxy reads HTTP(S)_PROXY and NO_PROXY implicitly and diverges from the spec's bypass dialect; install a ProxyOptionsWebProxy from an explicit ProxyOptions (CFG-28, P8b-4)

# The configuration binder (CFG-5, CFG-6, CFG-7, OBS-35; phase 9 P9-4, V1). Its source generator binds no init accessor and says
# nothing, and its reflection mode is not AOT-safe and mis-converts durations, integers and enums, so values reach the records
# through the DI package's own reader. The type arrives transitively with Microsoft.Extensions.Http: banned, not removed.
T:Microsoft.Extensions.Configuration.ConfigurationBinder;Bind through the DI package's reader; the binder's generator binds no init accessor and its reflection mode is not AOT-safe (CFG-5..CFG-7, P9-4)
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,8 +406,24 @@ Roadmap phase 7b, issue #10 (a later addition, not one of S1–S9), pinned by a
(`Content-Length: 0`, byte for byte), so `SocketsHttpHandler`'s own connection-failure retry cannot fire and the request reaches the server once. A body-less `GET`, `HEAD`, `OPTIONS`, `DELETE` and `TRACE` is
still re-sent by the native client (up to three more times), which is recorded as design §10 entry 33 and waived permanently for `transport-2.bodyless-not-resent`.

- **Guidance (phase 9, `XCUT-10`; documentation only):** composing `AddStandardResilienceHandler` under a named client is `o.Retry.ShouldHandle = _ => ValueTask.FromResult(false)`. The older guidance to set the SDK's own
`MaxRetryAttempts = 0` is withdrawn: the handler would still re-send a `POST` body behind the SDK's re-send gate, which measurably multiplies the attempts, and the handler-side `o.Retry.MaxRetryAttempts = 0` is refused by
the handler's own options validation. See `docs/sdk-documentation/dependency-injection.md`.

### Added

- **Phase 9 host integration (`CFG-1`, `-2`, `-3`, `-4`, `-5`, `-6`, `-7`, `-10`, `-11`, `-14`, `-37`, `-38`, `SEAM-5`, `SEAM-6`, `OBS-35`; design and plan under `docs/work/mvp/phase9/`):** the new package
`Dexpace.Sdk.Extensions.DependencyInjection`. `AddDexpaceClient(name[, IConfigurationSection])` returns a sealed `DexpaceClientBuilder` (`Configure`, `ConfigureTransport`, `ConfigureCredentials`, `UseTransport`, `UseSerde`,
`UseAuth`, `UseAuthPolicy`, `AddPolicy`, `UseProxyFromEnvironment` with and without a lookup, and `HttpClient`, the `IHttpClientBuilder` of the named client) and registers the client as keyed singletons (`HttpPipeline`,
`IAsyncHttpClient`, `ISerde`, `DexpaceClientOptions`). Settings are read by the package's own reader, not by the configuration binder (neither mode can serve: the generator binds no `init` accessor, the reflection mode is
not AOT-safe): base-10 integers, strict booleans, `CFG-7` durations (ISO-8601 day-time, shorthand, bare milliseconds, `none`), enums by name, lists that replace the defaults; an empty value is absent; every bad key, accessor
rejection and unknown key fails `ValidateOnStart` at once as `"<path>: <rule>"`, never quoting a configured value, and a host-less provider fails at first resolution. The named client is built over `IHttpClientFactory` with a
redirect-off primary, no factory loggers and an infinite handler lifetime with a pooled-connection lifetime, and a container-owned lease releases the primary. `DexpaceConfigurationKeys` publishes the conventional keys
(never read). One transport and one codec per client, explicit installs replace the defaults, a different second install throws at the call and keyed duplicates fail at start. `SystemNetHttpClient.CreatePrimaryHandler(options)`
is the one place the owned-handler invariants are built. `BannedSymbols.txt` bans `ConfigurationBinder` in `src/`; `scripts/ci/dependency-audit.cs` holds each library to a policy record (one third-party root, plus the
first-party projects it names). A third conformance driver runs the kit over the DI-built transport on both faces with the two permanent waivers; `NamedClientRedirectWireTests` is phase 1 S3's row 9; the AOT smoke covers
the package; `docs/sdk-documentation/dependency-injection.md`.

- `docs/sdk-documentation/tracing-and-metrics.md`; the AOT smoke covers the operation span, its attempt child and the two
metrics (`OBS-31`, `OBS-32`).
- `docs/sdk-documentation/pipelines.md`; the AOT smoke covers the synchronous pipeline, `ErrorMappingPolicy` and
Expand Down
18 changes: 14 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ dotnet format Dexpace.Sdk.sln --verify-no-changes # formatting gate
dotnet test --solution Dexpace.Sdk.sln --configuration Release
dotnet test --project tests/Dexpace.Sdk.Core.Tests --configuration Release --filter-trait "Category=Security"
dotnet test --project tests/Dexpace.Sdk.Http.SystemNet.Tests --configuration Release --filter-trait "Category=Conformance" # the kit against the reference transport
dotnet test --project tests/Dexpace.Sdk.Extensions.DependencyInjection.Tests --configuration Release --filter-trait "Category=Conformance" # the kit against the DI-built named-client transport
dotnet pack Dexpace.Sdk.sln --configuration Release --output artifacts/packages
```

Expand Down Expand Up @@ -77,7 +78,8 @@ tests and tools — targets **`net10.0` only**, set once in `Directory.Build.pro
`Microsoft.Extensions.Logging.Abstractions` (band-matched to the target, 10.0.x), and build-only packages
marked `PrivateAssets="all"`. The logging facade is a recorded deviation (`logging-abstractions-dependency`,
design §10); nobody re-litigates it, and nothing else is added. Adapters are held to the same rule plus
NFR-2's "at most one third-party library". `scripts/ci/dependency-audit.cs` enforces it on every pack.
NFR-2's "at most one third-party library", where a root package whose closure is one publisher's lock-stepped family counts as one (the DI package's is `Microsoft.Extensions.Http`, design §10 entry 2).
`scripts/ci/dependency-audit.cs` enforces it on every pack, from one policy record per library (its one third-party root and the first-party projects it may reference).
- **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` granted to the library's own test project only.
Expand Down Expand Up @@ -143,16 +145,18 @@ dotnet-sdk/
│ │ └── Errors/ # SdkException hierarchy, IRetryableError, OperationTimeoutException, RedirectException (+ downgrade / not-replayable leaves), SerdeException, ExceptionFacts, ExceptionTrail
│ ├── Dexpace.Sdk.Http.SystemNet/ # reference transport over System.Net.Http.HttpClient; SystemNetHttpClientOptions, HeaderDropLogging
│ ├── Dexpace.Sdk.Serialization.SystemTextJson/ # ISerde over source-generated System.Text.Json
│ ├── Dexpace.Sdk.Extensions.DependencyInjection/ # host integration: AddDexpaceClient, DexpaceClientBuilder, the configuration reader and its grammars, validation, the named client over IHttpClientFactory
│ └── Dexpace.Sdk.Conformance/ # the transport conformance kit (core only): TransportSuite, TransportSubject, the report and waivers, Wire/ (the loopback fixture)
├── tests/
│ ├── Dexpace.Sdk.Core.Tests/ # core + fakes only; Architecture/ holds SEAM-1, SEAM-2, SEAM-22, SSE-37 and SSE-38
│ ├── Dexpace.Sdk.Http.SystemNet.Tests/ # the transport, incl. wire tests over the kit's Wire/ server and SystemNetConformanceTests (the kit's driver over both constructions, with its two permanent waivers)
│ ├── Dexpace.Sdk.Conformance.Tests/ # the kit's own tests, a negative control per assertion (Controls/), the raw-socket client (RawSocket/) and its driver (Drivers/)
│ ├── Dexpace.Sdk.Serialization.SystemTextJson.Tests/
│ ├── Dexpace.Sdk.Extensions.DependencyInjection.Tests/ # the reader, registration, the named client, the third conformance driver (Conformance/) and NamedClientRedirectWireTests (Security/)
│ ├── Dexpace.Sdk.TestSupport/ # fake transports, time, diagnostics listeners (not packed)
│ └── Dexpace.Sdk.AotSmoke/ # NativeAOT smoke consumer, published and run in CI
├── scripts/
│ ├── ci/ # coverage-gate{.cs,-selftest.sh}, dependency-audit.cs, reproducible-pack.sh
│ ├── ci/ # coverage-gate{.cs,-selftest.sh}, dependency-audit.cs (one policy record per library), reproducible-pack.sh
│ └── knowledge # the knowledge-lookup CLI
├── tools/Dexpace.Tools.sln # tools/Knowledge{,.Tests} + .claude/skills/{housekeeping,knowledge-harvest}/{src,tests}
├── .github/ # ci.yml, labels.yml, dependabot.yml, CODEOWNERS, issue/PR templates
Expand Down Expand Up @@ -224,6 +228,11 @@ Layered, bottom-up:
`tests/Dexpace.Sdk.Conformance.Tests/Controls/` that it must fail (`AssertionCatalogueTests` fails an assertion with none); a leaked-socket control must root what it leaks, because a finalizer
closes it otherwise. A `ConformanceWaiver` whose assertion now passes fails the run, so fixing a requirement means deleting its waiver in the same change. The kit never calls `Task.Delay`, and a release check
needs a keep-alive reply, never a `Connection: close` one.
- **Host integration is configured by a reader, not the binder, and fails at start naming the key.** `Dexpace.Sdk.Extensions.DependencyInjection` reads the `IConfigurationSection` the caller hands `AddDexpaceClient` with its own grammars
(`ConfigurationBinder` is banned in `src/`: the generator binds no `init` accessor, the reflection mode is not AOT-safe); every bad value, rejected setting and unknown key fails `ValidateOnStart` at once as `"<path>: <rule>"`, never quoting a configured
value (a numeric accessor message may echo the number), and with no host `IStartupValidator.Validate()` or the first resolution fails the same way. An empty value is absent (it silently shadows a lower provider); there is no reload; a codec is required
(`UseSerde`); dispose the provider with `DisposeAsync` (a synchronous dispose throws for an async-only transport); no configuration value turns the environment proxy read on. Compose `AddStandardResilienceHandler` with
`o.Retry.ShouldHandle = _ => ValueTask.FromResult(false)`, never with the SDK's `MaxRetryAttempts = 0` (`docs/sdk-documentation/dependency-injection.md`).
- **Central Package Management is on.** Add new dependency versions to `Directory.Packages.props`, and
reference them without a `Version` attribute.

Expand Down Expand Up @@ -302,5 +311,6 @@ form — see `docs/sdk-documentation/pagination.md`; and phase 8a's transport co
the six result statuses and the by-ID waivers that must stay needed, the loopback `Wire` fixture promoted into it, the two drivers (the reference transport and a test-only raw-socket client) and the requirement
catalogue generated from appendix C — see `docs/sdk-documentation/conformance.md`; and phase 8b's hardening of the reference transport — the options record and four constructors, the handler check, the
installed proxy, the per-call deadline with its one failure classification, the real synchronous send, the header partition and drop policy, the bounded re-send capture and the `ObjectDisposedException` latch, with the kit green against both constructions and two permanent assertion-scoped
waivers (`TRANSPORT-14`'s malformed-name clause and `TRANSPORT-2`'s native retry for a body-less `GET`/`HEAD`/`OPTIONS`/`DELETE`/`TRACE`, design §10 entries 32 and 33) — see `docs/sdk-documentation/transport.md`): the DI package
`Dexpace.Sdk.Extensions.DependencyInjection` (9), and the release path (12).
waivers (`TRANSPORT-14`'s malformed-name clause and `TRANSPORT-2`'s native retry for a body-less `GET`/`HEAD`/`OPTIONS`/`DELETE`/`TRACE`, design §10 entries 32 and 33) — see `docs/sdk-documentation/transport.md`; and phase 9's host integration — the DI package `Dexpace.Sdk.Extensions.DependencyInjection`: `AddDexpaceClient` and the builder, the hand-written configuration reader and its grammars, the validator with `ValidateOnStart`, the named client over
`IHttpClientFactory` with its redirect-off primary and container-owned lease, the single-registration check, `DexpaceConfigurationKeys`, the audit's policy record, the third conformance driver — see `docs/sdk-documentation/dependency-injection.md`): only the cross-cutting invariant and
conformance suites (10), the deviation audit (11) and the release path (12).
30 changes: 30 additions & 0 deletions Dexpace.Sdk.sln
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dexpace.Sdk.Conformance", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dexpace.Sdk.Conformance.Tests", "tests\Dexpace.Sdk.Conformance.Tests\Dexpace.Sdk.Conformance.Tests.csproj", "{6BEC45D3-771D-4381-8FA8-B6A84A90EF33}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dexpace.Sdk.Extensions.DependencyInjection", "src\Dexpace.Sdk.Extensions.DependencyInjection\Dexpace.Sdk.Extensions.DependencyInjection.csproj", "{A7B97526-D016-42EE-9854-C9E8B979835A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dexpace.Sdk.Extensions.DependencyInjection.Tests", "tests\Dexpace.Sdk.Extensions.DependencyInjection.Tests\Dexpace.Sdk.Extensions.DependencyInjection.Tests.csproj", "{E75C4EEC-F6D7-42BF-9C80-1243EB327285}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -157,6 +161,30 @@ Global
{6BEC45D3-771D-4381-8FA8-B6A84A90EF33}.Release|x64.Build.0 = Release|Any CPU
{6BEC45D3-771D-4381-8FA8-B6A84A90EF33}.Release|x86.ActiveCfg = Release|Any CPU
{6BEC45D3-771D-4381-8FA8-B6A84A90EF33}.Release|x86.Build.0 = Release|Any CPU
{A7B97526-D016-42EE-9854-C9E8B979835A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A7B97526-D016-42EE-9854-C9E8B979835A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A7B97526-D016-42EE-9854-C9E8B979835A}.Debug|x64.ActiveCfg = Debug|Any CPU
{A7B97526-D016-42EE-9854-C9E8B979835A}.Debug|x64.Build.0 = Debug|Any CPU
{A7B97526-D016-42EE-9854-C9E8B979835A}.Debug|x86.ActiveCfg = Debug|Any CPU
{A7B97526-D016-42EE-9854-C9E8B979835A}.Debug|x86.Build.0 = Debug|Any CPU
{A7B97526-D016-42EE-9854-C9E8B979835A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A7B97526-D016-42EE-9854-C9E8B979835A}.Release|Any CPU.Build.0 = Release|Any CPU
{A7B97526-D016-42EE-9854-C9E8B979835A}.Release|x64.ActiveCfg = Release|Any CPU
{A7B97526-D016-42EE-9854-C9E8B979835A}.Release|x64.Build.0 = Release|Any CPU
{A7B97526-D016-42EE-9854-C9E8B979835A}.Release|x86.ActiveCfg = Release|Any CPU
{A7B97526-D016-42EE-9854-C9E8B979835A}.Release|x86.Build.0 = Release|Any CPU
{E75C4EEC-F6D7-42BF-9C80-1243EB327285}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E75C4EEC-F6D7-42BF-9C80-1243EB327285}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E75C4EEC-F6D7-42BF-9C80-1243EB327285}.Debug|x64.ActiveCfg = Debug|Any CPU
{E75C4EEC-F6D7-42BF-9C80-1243EB327285}.Debug|x64.Build.0 = Debug|Any CPU
{E75C4EEC-F6D7-42BF-9C80-1243EB327285}.Debug|x86.ActiveCfg = Debug|Any CPU
{E75C4EEC-F6D7-42BF-9C80-1243EB327285}.Debug|x86.Build.0 = Debug|Any CPU
{E75C4EEC-F6D7-42BF-9C80-1243EB327285}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E75C4EEC-F6D7-42BF-9C80-1243EB327285}.Release|Any CPU.Build.0 = Release|Any CPU
{E75C4EEC-F6D7-42BF-9C80-1243EB327285}.Release|x64.ActiveCfg = Release|Any CPU
{E75C4EEC-F6D7-42BF-9C80-1243EB327285}.Release|x64.Build.0 = Release|Any CPU
{E75C4EEC-F6D7-42BF-9C80-1243EB327285}.Release|x86.ActiveCfg = Release|Any CPU
{E75C4EEC-F6D7-42BF-9C80-1243EB327285}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -172,5 +200,7 @@ Global
{73508377-AAE0-4F22-BBCD-BFAEA76BD08E} = {892AF1FA-5CA2-4CB1-8286-F7BC8E8B6D93}
{A993A744-89E7-45DB-A33D-3A87620FA062} = {1345587B-8949-4F11-B5B7-5F5508DA63FF}
{6BEC45D3-771D-4381-8FA8-B6A84A90EF33} = {892AF1FA-5CA2-4CB1-8286-F7BC8E8B6D93}
{A7B97526-D016-42EE-9854-C9E8B979835A} = {1345587B-8949-4F11-B5B7-5F5508DA63FF}
{E75C4EEC-F6D7-42BF-9C80-1243EB327285} = {892AF1FA-5CA2-4CB1-8286-F7BC8E8B6D93}
EndGlobalSection
EndGlobal
10 changes: 10 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
net10.0 target: its 10.0.x net10.0 group resolves System.Diagnostics.DiagnosticSource to the in-box
assembly instead of a package copy. scripts/ci/dependency-audit.cs enforces the band. -->
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.12" />
<!-- The one third-party root of the hosting family, taken only by Dexpace.Sdk.Extensions.DependencyInjection (design §10
entry 2, phase 9 P9-3). Its closure is the 13 Microsoft.Extensions.* packages of phase 9's V5, all band-matched;
scripts/ci/dependency-audit.cs names it once, in the package's policy. -->
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.12" />
</ItemGroup>

<ItemGroup Label="Build-only analyzers (PrivateAssets=all)">
Expand All @@ -41,6 +45,12 @@
<PackageVersion Include="Microsoft.Testing.Extensions.HangDump" Version="2.4.1" />
<!-- The FakeTimeProvider of styleguide 11.6, re-exported to every suite by Dexpace.Sdk.TestSupport. -->
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="10.10.0" />
<!-- AddEnvironmentVariables(prefix) for CFG-3's test: the host's own "__" to ":" rule, proven rather than re-implemented (phase 9,
task 2.8). Not in the hosting family's closure, so it never reaches a library. -->
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.12" />
<!-- ResilienceCompositionTests (phase 9 P9-19, V6): the standard resilience handler composed under a named client, to pin the one-retry-layer
guidance (XCUT-10). Test-only and on its own 10.x line: nothing here reaches a library's closure. -->
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.10.0" />
</ItemGroup>

</Project>
Loading
Loading