diff --git a/CHANGELOG-CodeGenerators.md b/CHANGELOG-CodeGenerators.md index 7e9b83e..1785c9b 100644 --- a/CHANGELOG-CodeGenerators.md +++ b/CHANGELOG-CodeGenerators.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.4.1] - 2026-08-23 + +### Documentation + +- Added XML documentation comments to `HalResponseGenerator`, previously undocumented. The + companion `HalResponseAttribute` in the unpublished `Chatter.Rest.Hal.Core` project was + documented too. Documentation only: no API or behavior change. + ## [0.4.0] - 2026-08-22 ### Added @@ -74,5 +82,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - A `record` annotated with `[HalResponse]` previously generated nothing silently and now reports `HAL0003`. Declare the type as a `partial class` to receive the HAL members. -[Unreleased]: https://github.com/brenpike/Chatter.Rest.Hal/compare/codegen/v0.4.0...HEAD +[Unreleased]: https://github.com/brenpike/Chatter.Rest.Hal/compare/codegen/v0.4.1...HEAD +[0.4.1]: https://github.com/brenpike/Chatter.Rest.Hal/compare/codegen/v0.4.0...codegen/v0.4.1 [0.4.0]: https://github.com/brenpike/Chatter.Rest.Hal/compare/codegen/v0.3.0...codegen/v0.4.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 34b07e9..5c4defd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.1.1] - 2026-08-23 + +### Documentation + +- Added XML documentation comments to the 20 public fluent builder stage interfaces under + `Builders/Stages/`, which were previously undocumented. Documentation only: no API or + behavior change. +- Enabled `GenerateDocumentationFile` for `Chatter.Rest.Hal`, so the package now ships the XML + documentation sidecar (`lib/net8.0/Chatter.Rest.Hal.xml` and + `lib/netstandard2.0/Chatter.Rest.Hal.xml`). The doc comments above previously never reached + the published package; consumers now get IntelliSense from them. +- Fixed malformed doc comments surfaced by enabling generation: closed six unclosed + `` blocks in each of `IEmbeddedLinkObjectPropertiesSelectionStage` and + `IResourceLinkObjectPropertiesSelectionStage` (both also gained an interface summary and + documentation for their new-hiding `AsArray()` member), and replaced two unresolvable + `JsonNode.Deserialize` crefs with resolvable `JsonSerializer.Deserialize` references. + ## [2.1.0] - 2026-08-23 ### Fixed @@ -91,6 +108,7 @@ rejected. ([#120](https://github.com/brenpike/Chatter.Rest.Hal/issues/120), open: empty-string href tolerance). -[Unreleased]: https://github.com/brenpike/Chatter.Rest.Hal/compare/hal/v2.1.0...HEAD +[Unreleased]: https://github.com/brenpike/Chatter.Rest.Hal/compare/hal/v2.1.1...HEAD +[2.1.1]: https://github.com/brenpike/Chatter.Rest.Hal/compare/hal/v2.1.0...hal/v2.1.1 [2.1.0]: https://github.com/brenpike/Chatter.Rest.Hal/compare/hal/v2.0.0...hal/v2.1.0 [2.0.0]: https://github.com/brenpike/Chatter.Rest.Hal/compare/hal/v1.1.0...hal/v2.0.0 diff --git a/CLAUDE.md b/CLAUDE.md index e110cbd..c18a849 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,8 +19,8 @@ Chatter.Rest.Hal is a .NET/C# implementation of the HAL (Hypertext Application L | `docs/usage.md` | Copy-paste examples for building, serializing, deserializing | | `docs/development.md` | Build/test/pack commands, code style, test conventions, CI/CD | | `docs/HAL_TEST_PLAN.md` | HAL spec-to-test mapping; consult for spec compliance and tests | -| `docs/aspnetcore/requirements.md` | Package requirements for `Chatter.Rest.Hal.AspNetCore` | -| `docs/aspnetcore/architecture.md` | Architecture, pseudocode, and test strategy for `Chatter.Rest.Hal.AspNetCore` | +| `docs/aspnetcore/requirements.md` | Package requirements for `Chatter.Rest.Hal.AspNetCore` (planned package — design docs) | +| `docs/aspnetcore/architecture.md` | Architecture, pseudocode, and test strategy for `Chatter.Rest.Hal.AspNetCore` (planned package — design docs) | ## Solution Structure @@ -81,8 +81,8 @@ Observed conventions: | Package | Version | |---|---| -| `Chatter.Rest.Hal` | `2.1.0` | -| `Chatter.Rest.Hal.CodeGenerators` | `0.4.0` | +| `Chatter.Rest.Hal` | `2.1.1` | +| `Chatter.Rest.Hal.CodeGenerators` | `0.4.1` | External dependency: `Chatter.Rest.UriTemplates` v0.1.0. diff --git a/README.md b/README.md index 1558e5b..0df9a6d 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ Minimal, copy-paste example (build, serialize): using System; using System.Text.Json; using Chatter.Rest.Hal; +using Chatter.Rest.Hal.Builders; // Build a simple resource with state and a self link var resource = ResourceBuilder @@ -96,6 +97,8 @@ Console.WriteLine(json); */ ``` +> **Note:** `ResourceBuilder` lives in the `Chatter.Rest.Hal.Builders` namespace. Later snippets in this README omit the `using` directives for brevity; each builder example assumes `using Chatter.Rest.Hal;` and `using Chatter.Rest.Hal.Builders;`. + If you want compile-time helpers generated for your response types, also install the code generators package: ```bash @@ -276,11 +279,13 @@ var resource = ResourceBuilder.WithState(new { currentlyProcessing = 14, shipped "self": { "href": "/orders" }, - "curies": { - "href": "http://example.com/docs/rels/{rel}", - "templated": true, - "name": "ea" - }, + "curies": [ + { + "href": "http://example.com/docs/rels/{rel}", + "templated": true, + "name": "ea" + } + ], "next": { "href": "/orders?page=2" }, @@ -288,18 +293,24 @@ var resource = ResourceBuilder.WithState(new { currentlyProcessing = 14, shipped "href": "/orders{?id}", "templated": true }, - "ea:admin": { - "href": "/admins/2", - "title": "Fred" - } + "ea:admin": [ + { + "href": "/admins/2", + "title": "Fred" + }, + { + "href": "/admins/5", + "title": "Kate" + } + ] }, "_embedded": { "ea:order": [ { - "total": 10, - "currency": "USD", - "status": "shipped", - "id": "6d5edc98-8b81-435f-ad7a-a66a60d91bd2", + "Id": "6d5edc98-8b81-435f-ad7a-a66a60d91bd2", + "Total": 10, + "Currency": "USD", + "Status": "shipped", "_links": { "self": { "href": "/orders/6d5edc98-8b81-435f-ad7a-a66a60d91bd2" @@ -315,10 +326,10 @@ var resource = ResourceBuilder.WithState(new { currentlyProcessing = 14, shipped } }, { - "total": 20, - "currency": "CAD", - "status": "processing", - "id": "418845a7-ec41-4288-83eb-22a8bb22e472", + "Id": "418845a7-ec41-4288-83eb-22a8bb22e472", + "Total": 20, + "Currency": "CAD", + "Status": "processing", "_links": { "self": { "href": "/orders/418845a7-ec41-4288-83eb-22a8bb22e472" @@ -338,7 +349,7 @@ var resource = ResourceBuilder.WithState(new { currentlyProcessing = 14, shipped } ``` -> The output above shows two of the six orders. Remaining orders follow the same structure. +> The output above shows two of the six orders. Remaining orders follow the same structure. `Order` property names serialize with their C# casing (`Id`, `Total`, ...) because the example does not configure a JSON naming policy. --- @@ -468,7 +479,7 @@ var resources = resource!.GetResourceCollection("ea:order"); ### Get a Link by relation -Retrieve a single link by its relation. Returns `null` if not found, or throws an exception if multiple links with the same relation exist: +Retrieve a single link by its relation. Returns `null` if not found. The lookup uses `SingleOrDefault`, so it throws `InvalidOperationException` if multiple links share the same relation — though since 2.0.0 duplicate relations cannot arise: `LinkCollection.Add` rejects a duplicate relation key, and deserialization normalizes duplicate relations last-wins before they reach the collection. ```csharp var link = resource!.GetLinkOrDefault("self"); @@ -492,7 +503,7 @@ var linkObj = resource!.GetLinkObjectOrDefault("curies", "ea"); ### Get a Link Object of a Link by relation only -For relations with a single link object, retrieve it directly by relation: +Retrieve the first link object for a relation. If the relation holds several link objects, this returns the **first** one (it uses `FirstOrDefault`) rather than throwing: ```csharp var linkObj = resource!.GetLinkObjectOrDefault("self"); @@ -519,7 +530,6 @@ To force **all** link relations to serialize as JSON arrays regardless of count, ```csharp using Chatter.Rest.Hal; -using Chatter.Rest.Hal.Extensions; // ASP.NET Core services.AddControllers().AddJsonOptions(o => @@ -550,11 +560,10 @@ To force array representation for a **specific relation** only, use `AsArray()` ```csharp var resource = ResourceBuilder.WithState(new { total = 5 }) - .AddLinks() - .AddLink("orders").AsArray() // always emit as array - .AddLinkObject("/orders/1") - .AddSelf() // count-based (default behavior) - .AddLinkObject("/api/orders") + .AddLink("orders").AsArray() // always emit as array + .AddLinkObject("/orders/1") + .AddSelf() // count-based (default behavior) + .AddLinkObject("/api/orders") .Build(); ``` diff --git a/docs/api.md b/docs/api.md index e0cdce6..f5b88d2 100644 --- a/docs/api.md +++ b/docs/api.md @@ -164,13 +164,15 @@ public interface IAddResourceStage } ``` +> **Return-target semantics.** `AddResource()` / `AddResource(state)` return the newly added embedded resource — chained link/curies/embed calls configure that resource. `AddResources()` returns the collection builder instead: link/curies/embed calls chained on its return value target the resource that **owns** the `_embedded` entry, not each added item. Per-item links, curies, and nested embeds must be configured inside the `builder` callback. + --- ## 7. `IEmbeddedResourceCreationStage` Namespace: `Chatter.Rest.Hal.Builders.Stages.Embedded` -The embedded-context mirror of `IResourceCreationStage`. Returned by `AddResource()` / `AddResources()`. Supports the same link, curies, and embedded operations as the root resource stage, plus `Build()` which terminates the entire chain back to the root `Resource`. +The embedded-context mirror of `IResourceCreationStage`, returned by the `IAddResourceStage` methods. It exposes the same link, curies, and embedded operations as the root resource stage, plus `Build()` which terminates the entire chain back to the root `Resource` — but what those operations target depends on which method returned the stage. After `AddResource()` / `AddResource(state)` they configure the newly added embedded resource; after `AddResources()` the stage is the collection builder, so link/curies/embed calls target the resource that owns the `_embedded` entry and per-item configuration happens only inside the `AddResources` callback (see the return-target semantics note in Section 6). ```csharp public interface IEmbeddedResourceCreationStage : @@ -313,7 +315,9 @@ Extension methods on `Resource` for navigating links and embedded resources. ```csharp public static class ResourceExtensions { - // Find a link by relation. Returns null if not found. + // Find a link by relation. Returns null if not found. Implemented with + // SingleOrDefault: throws InvalidOperationException when multiple links + // share the same relation. public static Link? GetLinkOrDefault(this Resource resource, string relation); // Get all link objects for a relation. Returns null if the relation is not found. @@ -345,13 +349,17 @@ Extension methods on `LinkCollection`. ```csharp public static class LinkCollectionExtensions { - // Find a link by relation. Returns null if not found. + // Find a link by relation. Returns null if not found. Implemented with + // SingleOrDefault: throws InvalidOperationException when multiple links + // share the same relation. public static Link? GetLinkOrDefault(this LinkCollection links, string relation); // Get all link objects for a relation. Returns null if the relation is not found. public static LinkObjectCollection? GetLinkObjects(this LinkCollection links, string relation); // Get the first link object for a relation. Returns null if not found. + // Implemented with FirstOrDefault: a relation may legitimately carry + // several link objects, so this returns the first rather than throwing. public static LinkObject? GetLinkObjectOrDefault(this LinkCollection links, string linkRelation); // Get a named link object within a relation. Returns null if not found. @@ -359,8 +367,13 @@ public static class LinkCollectionExtensions // Expand a CURIE relation (e.g. "acme:widgets") to its full URI using the // "curies" link relation defined in this collection. Returns the original - // relation unchanged if no matching CURIE definition is found, the relation - // contains no colon, or the CURIE template lacks the {rel} token. + // relation unchanged when any of the following holds: + // - the relation contains no colon, or the colon is the first character; + // - the reference after the colon is empty (e.g. "acme:"); + // - no "curies" link relation exists, or no CURIE definition matches the + // prefix; + // - the matching CURIE definition is not marked "templated": true; + // - the CURIE template lacks the {rel} token. // Returns an empty string when relation is null or empty. public static string ExpandCurieRelation(this LinkCollection links, string relation); } @@ -376,6 +389,12 @@ Namespace: `Chatter.Rest.Hal` public static class LinkObjectCollectionExtensions { // Get a link object by its name property. Returns null if not found. + // Implemented with SingleOrDefault: throws InvalidOperationException when + // multiple link objects in the collection share the same name. Note the + // asymmetry with the by-relation overload + // GetLinkObjectOrDefault(LinkCollection, string linkRelation) in §11, which + // uses FirstOrDefault and never throws when a relation carries several + // link objects. public static LinkObject? GetLinkObjectOrDefault(this LinkObjectCollection linkObjects, string name); } @@ -435,7 +454,7 @@ public sealed class HalJsonOptions ### `JsonSerializerOptionsExtensions` -Namespace: `Chatter.Rest.Hal.Extensions` +Namespace: `Chatter.Rest.Hal` ```csharp public static class JsonSerializerOptionsExtensions @@ -498,7 +517,7 @@ var uri = link!.Expand(("q", "dotnet"), ("lang", "en")); ### Standalone `UriTemplate` Class -The `UriTemplate` class in `Chatter.Rest.UriTemplates` can be used independently of the HAL library for RFC 6570 Levels 1-3 template expansion. See [docs/uri-templates/usage.md](uri-templates/usage.md) for the full API reference and operator examples. +The `UriTemplate` class in `Chatter.Rest.UriTemplates` can be used independently of the HAL library for RFC 6570 Levels 1-3 template expansion. `Chatter.Rest.UriTemplates` is an external NuGet package dependency; see the [Chatter.Rest.UriTemplates package on NuGet](https://www.nuget.org/packages/Chatter.Rest.UriTemplates) for its API reference and operator examples. --- diff --git a/docs/architecture.md b/docs/architecture.md index e64491e..9d93d19 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -28,8 +28,8 @@ public sealed record Resource : IHalPart - `State()` — returns the resource state as `T`, materialized as a detached projection from the state of record (the constructor-supplied `JsonElement`, or the lazy `_stateCreator` delegate on the deserialization path) on every call. Projections never become the serialization source, so mutating a returned object affects neither serialization nor equality; a state supplied directly as `T` is the one exception and is returned by reference. Returns `null` on failure rather than throwing. - `As()` — converts the full `Resource` (including `_links`/`_embedded`) to `T`. A parsed resource converts from the retained source node; an in-memory resource is re-serialized on every call, so links, embedded resources and state added after an earlier `As()` call are reflected in the result. Use this to round-trip a HAL response into a typed DTO that declares `Links`/`Embedded` properties. -- **Lazy init via `Func` delegates** — the internal deserialization constructor (`internal Resource(JsonNode?, Func, Func, Func)`) stores the three factory delegates. `Links` and `Embedded` property getters invoke these delegates on first access and cache the result, making deserialization allocation-lazy. -- `StateObject` — internal property that drives `ResourceConverter.Write`. Its getter calls `State()`. +- **Init via `Func` delegates** — the internal deserialization constructor (`internal Resource(JsonNode?, Func, Func, Func)`) stores the three factory delegates. `Links` and `Embedded` property getters invoke these delegates on first access and cache the result. On the deserialization path `ResourceConverter.Read` materializes `_links`/`_embedded` eagerly and the delegates return those captured collections; only the state clone (`_stateCreator`) is deferred until first use. +- `CachedState` — internal property that drives `ResourceConverter.Write`. It returns the cached state object, materializing it from `_stateCreator` on first use, bypassing the defensive Link-guard checks in `State()` (safe on the write path only). `StateObject` is the sibling internal property whose getter calls `State()`. ### `Link` @@ -97,6 +97,8 @@ public sealed record EmbeddedResource : IHalPart Each collection wraps an internal `Collection` and delegates all `ICollection` operations to it. Each carries a `[JsonConverter]` attribute wiring to its own converter. +**Duplicate-key contract (2.0.0, #99/#104).** The two keyed collections enforce uniqueness on `Add`: `LinkCollection.Add` throws `ArgumentException` when a `Link` with the same `Rel` is already present, and `EmbeddedResourceCollection.Add` throws `ArgumentException` when an `EmbeddedResource` with the same `Name` is already present. Deserialization is deliberately asymmetric: `LinkCollectionConverter.Read` and `EmbeddedResourceCollectionConverter.Read` normalize duplicate keys last-wins before anything reaches the collection, so a duplicate rel/name in incoming JSON never surfaces as an `ArgumentException` — the last occurrence replaces the earlier one while keeping its position. + ### Containment Diagram ``` @@ -159,7 +161,7 @@ public abstract class HalBuilder : IBuildResource, IBuildHalPart()` | @@ -167,6 +169,12 @@ public abstract class HalBuilder : IBuildResource, IBuildHalPart` delegates: +Parses the entire JSON token into a `JsonNode` and requires a JSON object. `_links` and `_embedded` are materialized eagerly — via each collection converter's node-walking `ReadFromNode` path, or `Deserialize` when a custom converter is registered for that collection type — so a malformed member fails at the deserialization call rather than on a later property read. Three `Func` delegates are then passed to the internal `Resource` constructor: -- `stateCreator` — clones the node, removes `_links` and `_embedded`, returns the remaining `JsonObject`. -- `linksCreator` — deserializes `node["_links"]` as `LinkCollection`. -- `embeddedCreator` — deserializes `node["_embedded"]` as `EmbeddedResourceCollection`. +- `jsonObjectCreator` — clones the node, removes `_links` and `_embedded`, returns the remaining `JsonObject`; this clone is deferred until first use. +- `linkCollectionCreator` — returns the eagerly materialized `LinkCollection`. +- `embeddedCollectionCreator` — returns the eagerly materialized `EmbeddedResourceCollection`. -These delegates are passed to the internal `Resource` constructor and invoked only when the corresponding property is first accessed. +The property getters invoke these delegates on first access and cache the result; only the state clone is deferred work. **`LinkCollectionConverter.Read`** @@ -417,8 +424,8 @@ The package declares a dependency on `Chatter.Rest.Hal`, which supplies the `Lin > **Packable libraries need a direct runtime reference.** `PrivateAssets="all"` keeps the generator reference (and everything transitive under it, including `Chatter.Rest.Hal`) out of the nuspec produced by `dotnet pack`. An application project needs nothing more, but a class library that packs and exposes the generated `Links`/`Embedded` members must also add a direct, non-private `` so its own consumers restore the assembly those members are typed against: > > ```xml -> -> +> +> > ``` ### Known Limitations @@ -436,7 +443,7 @@ Chatter.Rest.Hal.sln │ ├── Chatter.Rest.Hal/ # Core library │ │ ├── depends on: System.Text.Json (inbox on net8.0, NuGet on netstandard2.0) │ │ ├── depends on: Chatter.Rest.UriTemplates (external NuGet package) -│ │ └── NuGet: Chatter.Rest.Hal v1.1.0 +│ │ └── NuGet: Chatter.Rest.Hal v2.1.1 │ │ │ ├── Chatter.Rest.Hal.Core/ # Shared attribute (unpublished) │ │ ├── contains: HalResponseAttribute only @@ -446,7 +453,7 @@ Chatter.Rest.Hal.sln │ └── Chatter.Rest.Hal.CodeGenerators/ # Roslyn source generator │ ├── depends on: Chatter.Rest.Hal (types used by generated source) │ ├── consumers add: -│ └── NuGet: Chatter.Rest.Hal.CodeGenerators v0.4.0 +│ └── NuGet: Chatter.Rest.Hal.CodeGenerators v0.4.1 │ └── test/ ├── Chatter.Rest.Hal.Tests/ # Tests for core library @@ -471,4 +478,4 @@ The core library references `Chatter.Rest.UriTemplates` as a NuGet package depen Level 4 modifiers (prefix `:N` and explode `*`) are explicitly deferred and throw `NotSupportedException` at parse time. -For the complete type design, operator reference, encoding rules, and integration details, see [docs/uri-templates/architecture.md](uri-templates/architecture.md). +For the complete type design, operator reference, encoding rules, and integration details, see the external [`Chatter.Rest.UriTemplates`](https://www.nuget.org/packages/Chatter.Rest.UriTemplates/) package and its repository documentation. diff --git a/docs/aspnetcore/architecture.md b/docs/aspnetcore/architecture.md index f948fd2..8730983 100644 --- a/docs/aspnetcore/architecture.md +++ b/docs/aspnetcore/architecture.md @@ -1,6 +1,8 @@ # Chatter.Rest.Hal.AspNetCore -- Architecture -This document describes how `Chatter.Rest.Hal.AspNetCore` is built. For what it does, see [requirements.md](requirements.md). +> **Status: PLANNED — design specification only.** The `Chatter.Rest.Hal.AspNetCore` package described in this document has NOT been implemented or published. No `src/Chatter.Rest.Hal.AspNetCore/` project exists in this repository yet. + +This document describes how the planned `Chatter.Rest.Hal.AspNetCore` package will be built. For what it will do, see [requirements.md](requirements.md). --- diff --git a/docs/aspnetcore/requirements.md b/docs/aspnetcore/requirements.md index a5d3b3e..9fedc40 100644 --- a/docs/aspnetcore/requirements.md +++ b/docs/aspnetcore/requirements.md @@ -1,16 +1,18 @@ # Chatter.Rest.Hal.AspNetCore -- Requirements -This document is the source of truth for what the `Chatter.Rest.Hal.AspNetCore` package does. Test scenarios are derived directly from the numbered requirements below. For how the system is built, see [architecture.md](architecture.md). +> **Status: PLANNED — design specification only.** The `Chatter.Rest.Hal.AspNetCore` package described in this document has NOT been implemented or published. No `src/Chatter.Rest.Hal.AspNetCore/` project exists in this repository yet. + +This document is the source of truth for what the planned `Chatter.Rest.Hal.AspNetCore` package will do. Test scenarios will be derived directly from the numbered requirements below. For how the system will be built, see [architecture.md](architecture.md). --- ## Overview -`Chatter.Rest.Hal.AspNetCore` is a new NuGet package that provides server-side ASP.NET Core integration for building HAL (Hypertext Application Language) APIs. It eliminates the manual wiring required to produce HAL responses from both MVC controllers and Minimal API endpoints, providing DI-friendly registration, a typed result type (`HalResult`) that works in both pipelines, a DI-injectable link builder (`IHalLinkBuilder`) backed by ASP.NET Core's `LinkGenerator`, opt-in auto-self link injection, and RFC 9457 Problem Details support via `IExceptionHandler`. +`Chatter.Rest.Hal.AspNetCore` is a planned NuGet package that will provide server-side ASP.NET Core integration for building HAL (Hypertext Application Language) APIs. It will eliminate the manual wiring required to produce HAL responses from both MVC controllers and Minimal API endpoints, providing DI-friendly registration, a typed result type (`HalResult`) that works in both pipelines, a DI-injectable link builder (`IHalLinkBuilder`) backed by ASP.NET Core's `LinkGenerator`, opt-in auto-self link injection, and RFC 9457 Problem Details support via `IExceptionHandler`. ### Package dependencies -- `Chatter.Rest.Hal` -- core HAL types (`Resource`, `Resource`, `LinkObject`, `LinkCollection`, `Link`) +- `Chatter.Rest.Hal` -- core HAL types (`Resource`, `Resource` (planned core addition — does not exist today), `LinkObject`, `LinkCollection`, `Link`) - `Microsoft.AspNetCore.App` -- ASP.NET Core framework reference (not a NuGet package; framework reference only) --- @@ -141,7 +143,7 @@ Already uses `Chatter.Rest.Hal` server-side for building HAL documents. Wants to ## Integration Story -The following shows two ways to integrate `Chatter.Rest.Hal.AspNetCore` in a .NET 8 application. +The following shows the two intended ways the planned `Chatter.Rest.Hal.AspNetCore` package would be integrated in a .NET 8 application. ### Option A -- Minimal API diff --git a/docs/development.md b/docs/development.md index b767a4f..2d68b45 100644 --- a/docs/development.md +++ b/docs/development.md @@ -10,7 +10,19 @@ Local setup, build commands, test conventions, and CI/CD parity for Chatter.Rest - **Language:** C# 10.0 (set per project via `10.0`) - No external tools beyond the .NET SDK are required -There is no `global.json` in this repository. The SDK version is pinned only in CI. Use any `8.0.x` SDK locally. +A `global.json` at the repo root pins the SDK: + +```json +{ + "sdk": { + "version": "8.0.100", + "rollForward": "latestMinor", + "allowPrerelease": false + } +} +``` + +Contributors need a stable (non-prerelease) .NET SDK `8.0.100` or any later `8.x` version — `rollForward: latestMinor` selects the highest installed `8.x` SDK. A 9.x-only install will not satisfy it. --- @@ -31,7 +43,8 @@ Chatter.Rest.Hal.sln **Target frameworks:** -- `src/` projects multi-target `net8.0;netstandard2.0` +- `Chatter.Rest.Hal` and `Chatter.Rest.Hal.Core` multi-target `net8.0;netstandard2.0` +- `Chatter.Rest.Hal.CodeGenerators` targets `netstandard2.0` only — Roslyn analyzers/source generators must target `netstandard2.0` so the compiler can load them on any host - `test/` projects target `net8.0` only --- diff --git a/docs/serialization.md b/docs/serialization.md index 61102c0..f724597 100644 --- a/docs/serialization.md +++ b/docs/serialization.md @@ -71,7 +71,7 @@ When `false` (the default), a link relation with a single `LinkObject` serialize ## 3. `AddHalConverters()` ```csharp -// Chatter.Rest.Hal.Extensions namespace +// Chatter.Rest.Hal namespace public static JsonSerializerOptions AddHalConverters( this JsonSerializerOptions options, HalJsonOptions? halOptions = null) @@ -94,7 +94,13 @@ Where `resolved` is `halOptions ?? HalJsonOptions.Default`. ### Idempotency -The method checks `options.Converters.OfType().Any()` before adding converters. Calling `AddHalConverters()` more than once on the same options instance is safe and a no-op after the first call. +The duplicate guard is applied **per converter type**: each of the eight converters is added only if no instance of that exact converter type is already registered on `options.Converters` (since 2.0.0, [#98](https://github.com/brenpike/Chatter.Rest.Hal/issues/98)). Calling `AddHalConverters()` more than once on the same options instance is safe. Consequences: + +- A consumer who pre-registered a **subset** of the HAL converters by hand gets only the missing ones added. +- A pre-registered converter is left exactly as registered, including its own `HalJsonOptions` — the `halOptions` argument does **not** retroactively apply to it. +- `halOptions` applies only to converters the call actually adds. + +Guarding on the exact HAL converter type — rather than on any converter handling the same domain type — keeps a consumer's own converter first in the list, where `JsonSerializer` continues to select it. ### Options resolution @@ -143,6 +149,20 @@ var resource = ResourceBuilder.WithState(new { }) Both paths converge at `LinkBuilder.SetIsArray()`, which sets `_isArray = true`. When `BuildPart()` is called, `Link.IsArray` is set to `true` on the resulting domain object. +#### `curies` is array-form by default + +A builder-created `curies` relation carries `IsArray = true` by default (since 2.0.0): `LinkBuilder` initializes `_isArray = rel == CuriesLink`, whichever builder path creates the relation — `AddCuries()`, `AddLink("curies")`, or a merge into either. HAL spec §8.3 establishes CURIEs via an array of Link Objects, so even a single CURIE definition serializes as an array. Calling `.AsArray()` on a `curies` relation is therefore a no-op. + +```json +{ + "_links": { + "curies": [ + { "href": "/docs/rels/{rel}", "name": "ea", "templated": true } + ] + } +} +``` + ### 4.2 Global — options **Option A:** Mutate `Default` at application startup: @@ -247,13 +267,13 @@ Every read path that materializes a Link Object from its **object form** — `{ ### 5.1 `ResourceConverter.Read` -Parses the entire JSON input into a `JsonNode` tree. Rather than immediately deserializing `_links` and `_embedded`, it captures three lazy `Func` delegates: +Parses the entire JSON input into a `JsonNode` tree and requires a JSON object. `_links` and `_embedded` are deserialized eagerly — via each collection converter's node-walking `ReadFromNode` path, or `Deserialize` when a custom converter is registered for that collection type — so a malformed member fails at the deserialization call rather than on a later property read. Three `Func` delegates are passed to the internal `Resource` constructor: -- `linkCollectionCreator` — deserializes `node["_links"]` on first access of `Resource.Links` -- `embeddedCollectionCreator` — deserializes `node["_embedded"]` on first access of `Resource.Embedded` -- `jsonObjectCreator` — clones the node, removes `_links` and `_embedded`, returns the remainder as the state `JsonObject` +- `linkCollectionCreator` — returns the eagerly deserialized `LinkCollection` +- `embeddedCollectionCreator` — returns the eagerly deserialized `EmbeddedResourceCollection` +- `jsonObjectCreator` — clones the node, removes `_links` and `_embedded`, returns the remainder as the state `JsonObject`; this clone is deferred until first use -These delegates are allocated once and evaluated only when the corresponding property is first accessed. This means deserialization of linked and embedded sub-graphs is deferred until needed. +The property getters invoke these delegates on first access and cache the result; only the state clone is deferred work. ### 5.2 `LinkCollectionConverter.Read` diff --git a/docs/usage.md b/docs/usage.md index c19391b..1312fa5 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -14,6 +14,7 @@ dotnet add package Chatter.Rest.Hal using System; using System.Text.Json; using Chatter.Rest.Hal; +using Chatter.Rest.Hal.Builders; var resource = ResourceBuilder .WithState(new { message = "Hello, HAL!" }) @@ -26,6 +27,8 @@ Console.WriteLine(json); This will print a HAL+JSON document with the state under the root and a `_links.self` link. +> **Note:** `ResourceBuilder` lives in the `Chatter.Rest.Hal.Builders` namespace. Later snippets omit the `using` directives for brevity; each builder example assumes `using Chatter.Rest.Hal;` and `using Chatter.Rest.Hal.Builders;`. + 2) Adding links and link objects Link objects contain metadata (href, title, templated, name etc.). Use the fluent API to express intent: @@ -43,11 +46,13 @@ var resource = ResourceBuilder.WithState(new { }) // add multiple link objects to the same relation .AddLink("ea:admin") - .AddLinkObject("/admins/2").WithTitle("Fred") - .AddLinkObject("/admins/5").WithTitle("Kate") + .AddLinkObject("/admins/2").WithName("fred").WithTitle("Fred") + .AddLinkObject("/admins/5").WithName("kate").WithTitle("Kate") .Build(); ``` +Note: since 2.0.0, `curies` always serializes as a JSON array — even when only a single CURIE definition is present — per HAL section 8.3. + 3) Embedding resources Embed a single resource or a collection. When embedding collections you can use AddResources and provide a lambda to configure each embedded resource's links. @@ -176,7 +181,7 @@ builder.Services.AddControllers() .AddJsonOptions(o => o.JsonSerializerOptions.AddHalConverters()); ``` -`AddHalConverters()` is idempotent — calling it more than once on the same `JsonSerializerOptions` instance is safe and has no effect after the first call. +`AddHalConverters()` is safe to call repeatedly on the same `JsonSerializerOptions` instance. The duplicate guard is applied per converter type: each call registers only the HAL converters not already present, so a consumer who registered some HAL converters by hand still gets the remaining ones. A converter that is already registered is left exactly as-is, including its `HalJsonOptions` — the `halOptions` argument applies only to converters the call actually adds. Note: when you supply explicit `JsonSerializerOptions` to `JsonSerializer`, the options-registered converters from `AddHalConverters()` take precedence over the attribute-wired converters. Both code paths produce identical HAL output under the same `HalJsonOptions`. @@ -205,8 +210,12 @@ var selfLink = resource.GetLinkOrDefault("self"); var selfLinkObject = resource.GetLinkObjectOrDefault("self"); var selfHref = selfLinkObject?.Href; -// Get a named link object within a relation (e.g., one of several ea:admin entries) -var adminLink = resource.GetLinkObjectOrDefault("ea:admin", "Fred"); +// Get a named link object within a relation (e.g., one of several ea:admin entries). +// Matching is on the link object's `name` property — set at build time via +// .WithName(...) (see section 2) — not on its title. The lookup is a +// SingleOrDefault over `name`: it returns null when no link object matches and +// throws when several link objects in the relation share the same name. +var adminLink = resource.GetLinkObjectOrDefault("ea:admin", "fred"); // Get the full collection of link objects for a relation var orderLinks = resource.GetLinkObjects("ea:order"); diff --git a/src/Chatter.Rest.Hal.CodeGenerators/Chatter.Rest.Hal.CodeGenerators.csproj b/src/Chatter.Rest.Hal.CodeGenerators/Chatter.Rest.Hal.CodeGenerators.csproj index 66e22c4..98f9c0a 100644 --- a/src/Chatter.Rest.Hal.CodeGenerators/Chatter.Rest.Hal.CodeGenerators.csproj +++ b/src/Chatter.Rest.Hal.CodeGenerators/Chatter.Rest.Hal.CodeGenerators.csproj @@ -12,7 +12,7 @@ dependency group has no matching lib folder by design. --> $(NoWarn);NU5128 - 0.4.0 + 0.4.1 Brennan Pike Brennan Pike Generates the appropriate additional attributes of a response object to match the HAL specification diff --git a/src/Chatter.Rest.Hal.CodeGenerators/HalResponseGenerator.cs b/src/Chatter.Rest.Hal.CodeGenerators/HalResponseGenerator.cs index 5dadc9e..f445c17 100644 --- a/src/Chatter.Rest.Hal.CodeGenerators/HalResponseGenerator.cs +++ b/src/Chatter.Rest.Hal.CodeGenerators/HalResponseGenerator.cs @@ -4,9 +4,24 @@ namespace Chatter.Rest.Hal.CodeGenerators; +/// +/// Incremental source generator that adds the reserved HAL members (Links, serialized as +/// _links, and Embedded, serialized as _embedded) to partial classes marked +/// with [HalResponse]. The pipeline has three stages: post-initialization emission of the +/// marker attribute itself (), discovery via +/// SyntaxProvider.ForAttributeWithMetadataName projecting each match onto the equatable +/// model, and a collected stage that deduplicates targets by metadata name +/// before renders the generated partials. +/// [Generator] public class HalResponseGenerator : IIncrementalGenerator { + /// + /// Registers the pipeline: emits the marker attribute as a post-initialization output, wires + /// attribute-driven discovery of annotated type declarations (reporting per-target diagnostics + /// as they surface), then collects, deduplicates by metadata name, orders deterministically, + /// and hands the resulting models to . + /// public void Initialize(IncrementalGeneratorInitializationContext context) { // Ship the marker attribute with the generator so installing this package alone is enough diff --git a/src/Chatter.Rest.Hal.Core/HalResponseAttribute.cs b/src/Chatter.Rest.Hal.Core/HalResponseAttribute.cs index 2e4b903..3dd42c8 100644 --- a/src/Chatter.Rest.Hal.Core/HalResponseAttribute.cs +++ b/src/Chatter.Rest.Hal.Core/HalResponseAttribute.cs @@ -2,6 +2,26 @@ namespace Chatter.Rest.Hal; +/// +/// Marks a partial class as a HAL resource so Chatter.Rest.Hal.CodeGenerators generates a +/// companion partial declaring the reserved HAL members: a Links property serialized as +/// _links and an Embedded property serialized as _embedded. +/// +/// +/// +/// Chatter.Rest.Hal.Core is not published as a standalone NuGet package. Since +/// Chatter.Rest.Hal.CodeGenerators 0.4.0 the generator emits its own internal sealed +/// copy of this attribute into every compilation it runs in, so installing the generator package +/// alone is enough for [HalResponse] to resolve. A project that references both this +/// assembly and the generator sees CS0436 (a source-declared type conflicting with an imported +/// type) and should drop the Chatter.Rest.Hal.Core reference. +/// +/// +/// As declared, the attribute is valid only on classes (); +/// AllowMultiple and Inherited are left at their +/// defaults (false and true respectively). +/// +/// [AttributeUsage(AttributeTargets.Class)] public class HalResponseAttribute : Attribute { diff --git a/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IAddCuriesLinkToEmbeddedStage.cs b/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IAddCuriesLinkToEmbeddedStage.cs index 6a34d15..3f99b8c 100644 --- a/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IAddCuriesLinkToEmbeddedStage.cs +++ b/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IAddCuriesLinkToEmbeddedStage.cs @@ -1,6 +1,23 @@ namespace Chatter.Rest.Hal.Builders.Stages.Embedded; +/// +/// A stage of the fluent builder from which the reserved "curies" link relation can be added to +/// the embedded resource being built +/// public interface IAddCuriesLinkToEmbeddedStage { + /// + /// Adds a "curies" link to the embedded resource being built. Repeated calls merge into the + /// single "curies" link, so each additional CURIE definition extends that link's array rather + /// than emitting a second "curies" entry. + /// + /// A to add CURIE s to the "curies" link + /// + /// https://datatracker.ietf.org/doc/html/draft-kelly-json-hal#section-8.2 + /// + /// 8.2. Custom link relation types can be represented via "CURIE syntax" links: a set of Link + /// Objects under the reserved "curies" link relation, each with a "name" (the CURIE prefix) + /// and a templated "href" containing the {rel} token. + /// IEmbeddedCuriesLinkCreationStage AddCuries(); } diff --git a/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IAddLinkToEmbeddedStage.cs b/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IAddLinkToEmbeddedStage.cs index 2f69fde..9350ba9 100644 --- a/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IAddLinkToEmbeddedStage.cs +++ b/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IAddLinkToEmbeddedStage.cs @@ -1,6 +1,26 @@ namespace Chatter.Rest.Hal.Builders.Stages.Embedded; +/// +/// A stage of the fluent builder from which a link with an arbitrary relation can be added to the +/// embedded resource being built +/// public interface IAddLinkToEmbeddedStage { + /// + /// Adds a link with the supplied relation to the embedded resource being built. Repeating a + /// relation merges its s into the single link for that relation, + /// because HAL's "_links" is a JSON object keyed by relation. + /// + /// The link relation type of the link. Must not be null or whitespace + /// A to add s to the link + /// + /// https://datatracker.ietf.org/doc/html/draft-kelly-json-hal#section-4.1.1 + /// + /// 4.1.1. The reserved "_links" property is OPTIONAL. + /// + /// It is an object whose property names are link relation types (as + /// defined by [RFC5988]) and values are either a Link Object or an array + /// of Link Objects. + /// IEmbeddedLinkCreationStage AddLink(string rel); } diff --git a/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IAddSelfLinkToEmbeddedStage.cs b/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IAddSelfLinkToEmbeddedStage.cs index a45dda9..0922093 100644 --- a/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IAddSelfLinkToEmbeddedStage.cs +++ b/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IAddSelfLinkToEmbeddedStage.cs @@ -1,6 +1,22 @@ namespace Chatter.Rest.Hal.Builders.Stages.Embedded; +/// +/// A stage of the fluent builder from which the reserved "self" link relation can be added to the +/// embedded resource being built +/// public interface IAddSelfLinkToEmbeddedStage { + /// + /// Adds a "self" link to the embedded resource being built. Repeated calls merge into the + /// single "self" link. + /// + /// A to add s to the "self" link + /// + /// https://datatracker.ietf.org/doc/html/draft-kelly-json-hal#section-4.1.1 + /// + /// 4.1.1. Each Resource Object SHOULD contain a 'self' link that corresponds + /// with the IANA registered 'self' relation (as defined by [RFC5988]) + /// whose target is the resource's URI. + /// IEmbeddedLinkCreationStage AddSelf(); } diff --git a/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IEmbeddedCuriesLinkCreationStage.cs b/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IEmbeddedCuriesLinkCreationStage.cs index a281245..64b9a85 100644 --- a/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IEmbeddedCuriesLinkCreationStage.cs +++ b/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IEmbeddedCuriesLinkCreationStage.cs @@ -1,7 +1,29 @@ namespace Chatter.Rest.Hal.Builders.Stages.Embedded; +/// +/// A stage of the fluent builder from which CURIE s can be added to the +/// "curies" link of the embedded resource being built +/// public interface IEmbeddedCuriesLinkCreationStage { + /// + /// Adds a CURIE with the supplied href and name to the "curies" link + /// + /// The REQUIRED href of the CURIE . A URI Template containing the {rel} token + /// The REQUIRED name of the CURIE , used as the prefix of compact link relation types + /// A to continue building the + /// + /// https://datatracker.ietf.org/doc/html/draft-kelly-json-hal#section-8.2 + /// + /// 8.2. Custom link relation types can be represented via "CURIE syntax" links: a set of Link + /// Objects under the reserved "curies" link relation, each with a "name" (the CURIE prefix) + /// and a templated "href" containing the {rel} token. + /// IEmbeddedLinkObjectPropertiesSelectionStage AddLinkObject(string href, string name); + /// + /// Forces the "curies" relation to serialize as a JSON array. No-op for curies since 2.0.0: + /// the curies relation is array-form by default per HAL section 8.3. + /// + /// A to continue building the "curies" link IEmbeddedCuriesLinkCreationStage AsArray(); } diff --git a/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IEmbeddedLinkCreationStage.cs b/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IEmbeddedLinkCreationStage.cs index c941c5b..0894c33 100644 --- a/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IEmbeddedLinkCreationStage.cs +++ b/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IEmbeddedLinkCreationStage.cs @@ -1,7 +1,37 @@ namespace Chatter.Rest.Hal.Builders.Stages.Embedded; +/// +/// A stage of the fluent builder from which s can be added to the link +/// being built on an embedded resource, or the link's relation forced to serialize as a JSON array +/// public interface IEmbeddedLinkCreationStage { + /// + /// Adds a with the supplied href to the link being built + /// + /// The REQUIRED href of the . Either a URI or a URI Template + /// A to continue building the + /// + /// https://datatracker.ietf.org/doc/html/draft-kelly-json-hal#section-5.1 + /// + /// 5.1. The "href" property is REQUIRED. + /// + /// Its value is either a URI [RFC3986] or a URI Template [RFC6570]. + /// + /// If the value is a URI Template then the Link Object SHOULD have a + /// "templated" attribute whose value is true. + /// IEmbeddedLinkObjectPropertiesSelectionStage AddLinkObject(string href); + /// + /// Forces the link relation being built to serialize as a JSON array even when it contains a + /// single + /// + /// A to continue building the link + /// + /// https://datatracker.ietf.org/doc/html/draft-kelly-json-hal#section-4.1.1 + /// + /// 4.1.1. The reserved "_links" property values are either a Link Object or an + /// array of Link Objects. + /// IEmbeddedLinkCreationStage AsArray(); } diff --git a/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IEmbeddedLinkObjectPropertiesSelectionStage.cs b/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IEmbeddedLinkObjectPropertiesSelectionStage.cs index f8dc262..b9c06c5 100644 --- a/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IEmbeddedLinkObjectPropertiesSelectionStage.cs +++ b/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IEmbeddedLinkObjectPropertiesSelectionStage.cs @@ -1,5 +1,10 @@ namespace Chatter.Rest.Hal.Builders.Stages.Embedded; +/// +/// A stage of the fluent builder from which the OPTIONAL properties of the +/// being built on an embedded resource can be set, or building can continue with further links, +/// link objects, resources or embedded resources +/// public interface IEmbeddedLinkObjectPropertiesSelectionStage : IEmbeddedLinkCreationStage, IEmbeddedCuriesLinkCreationStage, IAddLinkToEmbeddedStage, IAddSelfLinkToEmbeddedStage, IAddCuriesLinkToEmbeddedStage, IAddResourceStage, IAddEmbeddedResourceToResourceStage, IBuildResource { /// @@ -16,6 +21,7 @@ public interface IEmbeddedLinkObjectPropertiesSelectionStage : IEmbeddedLinkCrea /// /// Its value SHOULD be considered false if it is undefined or any other /// value than true. + /// IEmbeddedLinkObjectPropertiesSelectionStage Templated(); /// @@ -30,6 +36,7 @@ public interface IEmbeddedLinkObjectPropertiesSelectionStage : IEmbeddedLinkCrea /// /// Its value is a string used as a hint to indicate the media type /// expected when dereferencing the target resource. + /// IEmbeddedLinkObjectPropertiesSelectionStage WithType(string type); /// @@ -51,6 +58,7 @@ public interface IEmbeddedLinkObjectPropertiesSelectionStage : IEmbeddedLinkCrea /// property.The notification SHOULD include the deprecation property's /// value so that a client manitainer can easily find information about /// the deprecation. + /// IEmbeddedLinkObjectPropertiesSelectionStage WithDeprecationUrl(string deprecation); /// @@ -79,6 +87,7 @@ public interface IEmbeddedLinkObjectPropertiesSelectionStage : IEmbeddedLinkCrea /// /// Its value is a string which is a URI that hints about the profile(as /// defined by [I-D.wilde-profile-link]) of the target resource. + /// IEmbeddedLinkObjectPropertiesSelectionStage WithProfileUri(string profile); /// @@ -93,6 +102,7 @@ public interface IEmbeddedLinkObjectPropertiesSelectionStage : IEmbeddedLinkCrea /// /// Its value is a string and is intended for labelling the link with a /// human-readable identifier(as defined by [RFC5988]). + /// IEmbeddedLinkObjectPropertiesSelectionStage WithTitle(string title); /// @@ -107,6 +117,19 @@ public interface IEmbeddedLinkObjectPropertiesSelectionStage : IEmbeddedLinkCrea /// /// Its value is a string and is intended for indicating the language of /// the target resource(as defined by [RFC5988]). + /// IEmbeddedLinkObjectPropertiesSelectionStage WithHreflang(string hreflang); + /// + /// Forces the link relation being built to serialize as a JSON array even when it contains a + /// single . No-op for the "curies" relation, which is array-form by + /// default since 2.0.0 per HAL section 8.3 + /// + /// A to continue building the + /// + /// https://datatracker.ietf.org/doc/html/draft-kelly-json-hal#section-4.1.1 + /// + /// 4.1.1. The reserved "_links" property values are either a Link Object or an + /// array of Link Objects. + /// new IEmbeddedLinkObjectPropertiesSelectionStage AsArray(); } diff --git a/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IEmbeddedResourceCreationStage.cs b/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IEmbeddedResourceCreationStage.cs index 2f724b3..565ea76 100644 --- a/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IEmbeddedResourceCreationStage.cs +++ b/src/Chatter.Rest.Hal/Builders/Stages/Embedded/IEmbeddedResourceCreationStage.cs @@ -1,5 +1,16 @@ namespace Chatter.Rest.Hal.Builders.Stages.Embedded; +/// +/// The embedded-context mirror of , returned by the +/// methods. What subsequent link/embed calls target depends +/// on which method returned the stage: after +/// (or its state overload) they configure the newly added embedded resource; after +/// the stage is the collection builder, so +/// "self"/link/curies additions target the resource that OWNS the "_embedded" entry and nested +/// embed calls delegate to that owning resource — per-item configuration happens only inside the +/// AddResources callback. The caller can also add sibling resources or build the root +/// resource from this stage. +/// public interface IEmbeddedResourceCreationStage : IBuildHalPart, IAddLinkToEmbeddedStage, IAddSelfLinkToEmbeddedStage, IAddCuriesLinkToEmbeddedStage, IAddResourceStage, IAddEmbeddedResourceToResourceStage, IBuildResource { } diff --git a/src/Chatter.Rest.Hal/Builders/Stages/IAddCuriesLinkStage.cs b/src/Chatter.Rest.Hal/Builders/Stages/IAddCuriesLinkStage.cs index f74d479..e5ab1a0 100644 --- a/src/Chatter.Rest.Hal/Builders/Stages/IAddCuriesLinkStage.cs +++ b/src/Chatter.Rest.Hal/Builders/Stages/IAddCuriesLinkStage.cs @@ -1,6 +1,26 @@ namespace Chatter.Rest.Hal.Builders.Stages; +/// +/// A stage of the fluent builder from which the reserved "curies" link relation can be added to the +/// link collection being built. Context-neutral counterpart of +/// and +/// , implemented by builders that serve both +/// the resource and embedded contexts. +/// public interface IAddCuriesLinkStage { + /// + /// Adds a "curies" link to the link collection being built. Repeated calls merge into the + /// single "curies" link, so each additional CURIE definition extends that link's array rather + /// than emitting a second "curies" entry. + /// + /// A to add CURIE s to the "curies" link + /// + /// https://datatracker.ietf.org/doc/html/draft-kelly-json-hal#section-8.2 + /// + /// 8.2. Custom link relation types can be represented via "CURIE syntax" links: a set of Link + /// Objects under the reserved "curies" link relation, each with a "name" (the CURIE prefix) + /// and a templated "href" containing the {rel} token. + /// ICuriesLinkCreationStage AddCuries(); } diff --git a/src/Chatter.Rest.Hal/Builders/Stages/IAddEmbeddedResourceToResourceStage.cs b/src/Chatter.Rest.Hal/Builders/Stages/IAddEmbeddedResourceToResourceStage.cs index 58c9d4d..d0c29de 100644 --- a/src/Chatter.Rest.Hal/Builders/Stages/IAddEmbeddedResourceToResourceStage.cs +++ b/src/Chatter.Rest.Hal/Builders/Stages/IAddEmbeddedResourceToResourceStage.cs @@ -2,7 +2,26 @@ namespace Chatter.Rest.Hal.Builders.Stages; +/// +/// A stage of the fluent builder from which a named embedded resource entry can be added to the +/// "_embedded" property of the resource being built. Available from both the root-resource and +/// embedded builder contexts. +/// public interface IAddEmbeddedResourceToResourceStage { + /// + /// Adds an embedded resource entry with the supplied name to the resource being built + /// + /// The name that keys the entry within the resource's "_embedded" property + /// An to add one or more resources to the named entry + /// + /// https://datatracker.ietf.org/doc/html/draft-kelly-json-hal#section-4.1.2 + /// + /// 4.1.2. The reserved "_embedded" property is OPTIONAL. + /// + /// It is an object whose property names are link relation types (as + /// defined by [RFC5988]) and values are either a Resource Object or an + /// array of Resource Objects. + /// IAddResourceStage AddEmbedded(string name); } diff --git a/src/Chatter.Rest.Hal/Builders/Stages/IAddLinkStage.cs b/src/Chatter.Rest.Hal/Builders/Stages/IAddLinkStage.cs index 5a8cdc9..fdf7494 100644 --- a/src/Chatter.Rest.Hal/Builders/Stages/IAddLinkStage.cs +++ b/src/Chatter.Rest.Hal/Builders/Stages/IAddLinkStage.cs @@ -1,6 +1,28 @@ namespace Chatter.Rest.Hal.Builders.Stages; +/// +/// A stage of the fluent builder from which a link with an arbitrary relation can be added to the +/// link collection being built. Context-neutral counterpart of +/// and , +/// implemented by builders that serve both the resource and embedded contexts. +/// public interface IAddLinkStage { + /// + /// Adds a link with the supplied relation to the link collection being built. Repeating a + /// relation merges its s into the single link for that relation, + /// because HAL's "_links" is a JSON object keyed by relation. + /// + /// The link relation type of the link. Must not be null or whitespace + /// A to add s to the link + /// + /// https://datatracker.ietf.org/doc/html/draft-kelly-json-hal#section-4.1.1 + /// + /// 4.1.1. The reserved "_links" property is OPTIONAL. + /// + /// It is an object whose property names are link relation types (as + /// defined by [RFC5988]) and values are either a Link Object or an array + /// of Link Objects. + /// ILinkCreationStage AddLink(string rel); } diff --git a/src/Chatter.Rest.Hal/Builders/Stages/IAddResourceStage.cs b/src/Chatter.Rest.Hal/Builders/Stages/IAddResourceStage.cs index 663bf83..cf02d65 100644 --- a/src/Chatter.Rest.Hal/Builders/Stages/IAddResourceStage.cs +++ b/src/Chatter.Rest.Hal/Builders/Stages/IAddResourceStage.cs @@ -4,9 +4,38 @@ namespace Chatter.Rest.Hal.Builders.Stages; +/// +/// A stage of the fluent builder, returned by +/// , from which individual +/// resources or a batch of resources can be added to the named "_embedded" entry. +/// public interface IAddResourceStage { + /// + /// Adds a resource with no state to the "_embedded" entry being built + /// + /// An to build the new embedded resource IEmbeddedResourceCreationStage AddResource(); + /// + /// Adds a resource with the supplied state to the "_embedded" entry being built + /// + /// The OPTIONAL state object whose properties become the embedded resource's state + /// An to build the new embedded resource IEmbeddedResourceCreationStage AddResource(object? state); + /// + /// Adds one resource per item of to the "_embedded" entry being + /// built, each item becoming that resource's state. Also forces the entry to serialize as a + /// JSON array even when it contains a single resource. + /// + /// The type of the items in + /// The items to add as embedded resources + /// An OPTIONAL callback invoked with each item and its + /// to configure per-resource links, curies, and + /// embedded resources + /// An to continue building. Unlike + /// , the returned stage is the collection builder, NOT one of the + /// added resources: link/curies/embed calls chained on it target the resource that owns the + /// "_embedded" entry. Per-resource configuration happens only inside the + /// callback IEmbeddedResourceCreationStage AddResources(IEnumerable resources, Action? builder = null); } diff --git a/src/Chatter.Rest.Hal/Builders/Stages/IAddSelfLinkStage.cs b/src/Chatter.Rest.Hal/Builders/Stages/IAddSelfLinkStage.cs index d936eb0..2050583 100644 --- a/src/Chatter.Rest.Hal/Builders/Stages/IAddSelfLinkStage.cs +++ b/src/Chatter.Rest.Hal/Builders/Stages/IAddSelfLinkStage.cs @@ -1,6 +1,25 @@ namespace Chatter.Rest.Hal.Builders.Stages; +/// +/// A stage of the fluent builder from which the reserved "self" link relation can be added to the +/// link collection being built. Context-neutral counterpart of +/// and +/// , implemented by builders that serve both the +/// resource and embedded contexts. +/// public interface IAddSelfLinkStage { + /// + /// Adds a "self" link to the link collection being built. Repeated calls merge into the + /// single "self" link. + /// + /// A to add s to the "self" link + /// + /// https://datatracker.ietf.org/doc/html/draft-kelly-json-hal#section-4.1.1 + /// + /// 4.1.1. Each Resource Object SHOULD contain a 'self' link that corresponds + /// with the IANA registered 'self' relation (as defined by [RFC5988]) + /// whose target is the resource's URI. + /// ILinkCreationStage AddSelf(); } diff --git a/src/Chatter.Rest.Hal/Builders/Stages/ICuriesLinkCreationStage.cs b/src/Chatter.Rest.Hal/Builders/Stages/ICuriesLinkCreationStage.cs index dfcdb5d..0ea922d 100644 --- a/src/Chatter.Rest.Hal/Builders/Stages/ICuriesLinkCreationStage.cs +++ b/src/Chatter.Rest.Hal/Builders/Stages/ICuriesLinkCreationStage.cs @@ -3,6 +3,12 @@ namespace Chatter.Rest.Hal.Builders.Stages; +/// +/// A stage of the fluent builder from which CURIE s can be added to the +/// "curies" link being built. Context-neutral union of +/// and , +/// implemented by builders that serve both the resource and embedded contexts. +/// public interface ICuriesLinkCreationStage : IResourceCuriesLinkCreationStage, IEmbeddedCuriesLinkCreationStage { } diff --git a/src/Chatter.Rest.Hal/Builders/Stages/ILinkCreationStage.cs b/src/Chatter.Rest.Hal/Builders/Stages/ILinkCreationStage.cs index e5ab20d..d650083 100644 --- a/src/Chatter.Rest.Hal/Builders/Stages/ILinkCreationStage.cs +++ b/src/Chatter.Rest.Hal/Builders/Stages/ILinkCreationStage.cs @@ -3,6 +3,12 @@ namespace Chatter.Rest.Hal.Builders.Stages; +/// +/// A stage of the fluent builder from which s can be added to the link +/// being built, or the link's relation forced to serialize as a JSON array. Context-neutral union +/// of and , +/// implemented by builders that serve both the resource and embedded contexts. +/// public interface ILinkCreationStage : IResourceLinkCreationStage, IEmbeddedLinkCreationStage { } diff --git a/src/Chatter.Rest.Hal/Builders/Stages/ILinkObjectPropertiesSelectionStage.cs b/src/Chatter.Rest.Hal/Builders/Stages/ILinkObjectPropertiesSelectionStage.cs index fea8fae..3f32f51 100644 --- a/src/Chatter.Rest.Hal/Builders/Stages/ILinkObjectPropertiesSelectionStage.cs +++ b/src/Chatter.Rest.Hal/Builders/Stages/ILinkObjectPropertiesSelectionStage.cs @@ -3,6 +3,14 @@ namespace Chatter.Rest.Hal.Builders.Stages; +/// +/// A stage of the fluent builder from which the OPTIONAL HAL properties of the +/// being built can be set, or the chain re-entered to add further links, +/// curies, or embedded resources. Context-neutral union of +/// and +/// , implemented by builders that serve +/// both the resource and embedded contexts. +/// public interface ILinkObjectPropertiesSelectionStage : IResourceLinkObjectPropertiesSelectionStage, IEmbeddedLinkObjectPropertiesSelectionStage, IBuildResource { } diff --git a/src/Chatter.Rest.Hal/Builders/Stages/Resource/IAddCuriesLinkToResourceStage.cs b/src/Chatter.Rest.Hal/Builders/Stages/Resource/IAddCuriesLinkToResourceStage.cs index 0370491..f56e7fc 100644 --- a/src/Chatter.Rest.Hal/Builders/Stages/Resource/IAddCuriesLinkToResourceStage.cs +++ b/src/Chatter.Rest.Hal/Builders/Stages/Resource/IAddCuriesLinkToResourceStage.cs @@ -1,6 +1,23 @@ namespace Chatter.Rest.Hal.Builders.Stages.Resource; +/// +/// A stage of the fluent builder from which the reserved "curies" link relation can be added to +/// the resource being built +/// public interface IAddCuriesLinkToResourceStage { + /// + /// Adds a "curies" link to the resource being built. Repeated calls merge into the single + /// "curies" link, so each additional CURIE definition extends that link's array rather than + /// emitting a second "curies" entry. + /// + /// A to add CURIE s to the "curies" link + /// + /// https://datatracker.ietf.org/doc/html/draft-kelly-json-hal#section-8.2 + /// + /// 8.2. Custom link relation types can be represented via "CURIE syntax" links: a set of Link + /// Objects under the reserved "curies" link relation, each with a "name" (the CURIE prefix) + /// and a templated "href" containing the {rel} token. + /// IResourceCuriesLinkCreationStage AddCuries(); } diff --git a/src/Chatter.Rest.Hal/Builders/Stages/Resource/IAddLinkToResourceStage.cs b/src/Chatter.Rest.Hal/Builders/Stages/Resource/IAddLinkToResourceStage.cs index 73642ef..eea9f81 100644 --- a/src/Chatter.Rest.Hal/Builders/Stages/Resource/IAddLinkToResourceStage.cs +++ b/src/Chatter.Rest.Hal/Builders/Stages/Resource/IAddLinkToResourceStage.cs @@ -1,6 +1,26 @@ namespace Chatter.Rest.Hal.Builders.Stages.Resource; +/// +/// A stage of the fluent builder from which a link with an arbitrary relation can be added to the +/// resource being built +/// public interface IAddLinkToResourceStage { + /// + /// Adds a link with the supplied relation to the resource being built. Repeating a relation + /// merges its s into the single link for that relation, because HAL's + /// "_links" is a JSON object keyed by relation. + /// + /// The link relation type of the link. Must not be null or whitespace + /// A to add s to the link + /// + /// https://datatracker.ietf.org/doc/html/draft-kelly-json-hal#section-4.1.1 + /// + /// 4.1.1. The reserved "_links" property is OPTIONAL. + /// + /// It is an object whose property names are link relation types (as + /// defined by [RFC5988]) and values are either a Link Object or an array + /// of Link Objects. + /// IResourceLinkCreationStage AddLink(string rel); } diff --git a/src/Chatter.Rest.Hal/Builders/Stages/Resource/IAddSelfLinkToResourceStage.cs b/src/Chatter.Rest.Hal/Builders/Stages/Resource/IAddSelfLinkToResourceStage.cs index f559b36..34afeed 100644 --- a/src/Chatter.Rest.Hal/Builders/Stages/Resource/IAddSelfLinkToResourceStage.cs +++ b/src/Chatter.Rest.Hal/Builders/Stages/Resource/IAddSelfLinkToResourceStage.cs @@ -1,6 +1,22 @@ namespace Chatter.Rest.Hal.Builders.Stages.Resource; +/// +/// A stage of the fluent builder from which the reserved "self" link relation can be added to the +/// resource being built +/// public interface IAddSelfLinkToResourceStage { + /// + /// Adds a "self" link to the resource being built. Repeated calls merge into the single + /// "self" link. + /// + /// A to add s to the "self" link + /// + /// https://datatracker.ietf.org/doc/html/draft-kelly-json-hal#section-4.1.1 + /// + /// 4.1.1. Each Resource Object SHOULD contain a 'self' link that corresponds + /// with the IANA registered 'self' relation (as defined by [RFC5988]) + /// whose target is the resource's URI. + /// IResourceLinkCreationStage AddSelf(); } diff --git a/src/Chatter.Rest.Hal/Builders/Stages/Resource/IResourceCreationStage.cs b/src/Chatter.Rest.Hal/Builders/Stages/Resource/IResourceCreationStage.cs index eca7cd4..bfd8973 100644 --- a/src/Chatter.Rest.Hal/Builders/Stages/Resource/IResourceCreationStage.cs +++ b/src/Chatter.Rest.Hal/Builders/Stages/Resource/IResourceCreationStage.cs @@ -1,5 +1,10 @@ namespace Chatter.Rest.Hal.Builders.Stages.Resource; +/// +/// The root stage of the fluent builder, returned by the +/// entry points. From this stage the caller can add a "self" link, links with arbitrary +/// relations, a "curies" link, or named embedded resource entries, or build the resource. +/// public interface IResourceCreationStage : IAddCuriesLinkToResourceStage, IAddLinkToResourceStage, IAddSelfLinkToResourceStage, IAddEmbeddedResourceToResourceStage, IBuildResource { } diff --git a/src/Chatter.Rest.Hal/Builders/Stages/Resource/IResourceCuriesLinkCreationStage.cs b/src/Chatter.Rest.Hal/Builders/Stages/Resource/IResourceCuriesLinkCreationStage.cs index 8185404..3262e14 100644 --- a/src/Chatter.Rest.Hal/Builders/Stages/Resource/IResourceCuriesLinkCreationStage.cs +++ b/src/Chatter.Rest.Hal/Builders/Stages/Resource/IResourceCuriesLinkCreationStage.cs @@ -1,7 +1,29 @@ namespace Chatter.Rest.Hal.Builders.Stages.Resource; +/// +/// A stage of the fluent builder from which CURIE s can be added to the +/// "curies" link of the resource being built +/// public interface IResourceCuriesLinkCreationStage { + /// + /// Adds a CURIE with the supplied href and name to the "curies" link + /// + /// The REQUIRED href of the CURIE . A URI Template containing the {rel} token + /// The REQUIRED name of the CURIE , used as the prefix of compact link relation types + /// A to continue building the + /// + /// https://datatracker.ietf.org/doc/html/draft-kelly-json-hal#section-8.2 + /// + /// 8.2. Custom link relation types can be represented via "CURIE syntax" links: a set of Link + /// Objects under the reserved "curies" link relation, each with a "name" (the CURIE prefix) + /// and a templated "href" containing the {rel} token. + /// IResourceLinkObjectPropertiesSelectionStage AddLinkObject(string href, string name); + /// + /// Forces the "curies" relation to serialize as a JSON array. No-op for curies since 2.0.0: + /// the curies relation is array-form by default per HAL section 8.3. + /// + /// A to continue building the "curies" link IResourceCuriesLinkCreationStage AsArray(); } diff --git a/src/Chatter.Rest.Hal/Builders/Stages/Resource/IResourceLinkCreationStage.cs b/src/Chatter.Rest.Hal/Builders/Stages/Resource/IResourceLinkCreationStage.cs index 007da5d..1bd08e6 100644 --- a/src/Chatter.Rest.Hal/Builders/Stages/Resource/IResourceLinkCreationStage.cs +++ b/src/Chatter.Rest.Hal/Builders/Stages/Resource/IResourceLinkCreationStage.cs @@ -1,7 +1,37 @@ namespace Chatter.Rest.Hal.Builders.Stages.Resource; +/// +/// A stage of the fluent builder from which s can be added to the link +/// being built, or the link's relation forced to serialize as a JSON array +/// public interface IResourceLinkCreationStage { + /// + /// Adds a with the supplied href to the link being built + /// + /// The REQUIRED href of the . Either a URI or a URI Template + /// A to continue building the + /// + /// https://datatracker.ietf.org/doc/html/draft-kelly-json-hal#section-5.1 + /// + /// 5.1. The "href" property is REQUIRED. + /// + /// Its value is either a URI [RFC3986] or a URI Template [RFC6570]. + /// + /// If the value is a URI Template then the Link Object SHOULD have a + /// "templated" attribute whose value is true. + /// IResourceLinkObjectPropertiesSelectionStage AddLinkObject(string href); + /// + /// Forces the link relation being built to serialize as a JSON array even when it contains a + /// single + /// + /// A to continue building the link + /// + /// https://datatracker.ietf.org/doc/html/draft-kelly-json-hal#section-4.1.1 + /// + /// 4.1.1. The reserved "_links" property values are either a Link Object or an + /// array of Link Objects. + /// IResourceLinkCreationStage AsArray(); } diff --git a/src/Chatter.Rest.Hal/Builders/Stages/Resource/IResourceLinkObjectPropertiesSelectionStage.cs b/src/Chatter.Rest.Hal/Builders/Stages/Resource/IResourceLinkObjectPropertiesSelectionStage.cs index ce546c7..5bc006c 100644 --- a/src/Chatter.Rest.Hal/Builders/Stages/Resource/IResourceLinkObjectPropertiesSelectionStage.cs +++ b/src/Chatter.Rest.Hal/Builders/Stages/Resource/IResourceLinkObjectPropertiesSelectionStage.cs @@ -1,5 +1,9 @@ namespace Chatter.Rest.Hal.Builders.Stages.Resource; +/// +/// A stage of the fluent builder from which the OPTIONAL properties of the +/// being built can be set, further links or embedded resources added, or the built +/// public interface IResourceLinkObjectPropertiesSelectionStage : IResourceLinkCreationStage, IResourceCuriesLinkCreationStage, IAddLinkToResourceStage, IAddSelfLinkToResourceStage, IAddCuriesLinkToResourceStage, IAddEmbeddedResourceToResourceStage, IBuildResource { /// @@ -16,6 +20,7 @@ public interface IResourceLinkObjectPropertiesSelectionStage : IResourceLinkCrea /// /// Its value SHOULD be considered false if it is undefined or any other /// value than true. + /// IResourceLinkObjectPropertiesSelectionStage Templated(); /// @@ -30,6 +35,7 @@ public interface IResourceLinkObjectPropertiesSelectionStage : IResourceLinkCrea /// /// Its value is a string used as a hint to indicate the media type /// expected when dereferencing the target resource. + /// IResourceLinkObjectPropertiesSelectionStage WithType(string type); /// @@ -51,6 +57,7 @@ public interface IResourceLinkObjectPropertiesSelectionStage : IResourceLinkCrea /// property.The notification SHOULD include the deprecation property's /// value so that a client manitainer can easily find information about /// the deprecation. + /// IResourceLinkObjectPropertiesSelectionStage WithDeprecationUrl(string deprecation); /// @@ -79,6 +86,7 @@ public interface IResourceLinkObjectPropertiesSelectionStage : IResourceLinkCrea /// /// Its value is a string which is a URI that hints about the profile(as /// defined by [I-D.wilde-profile-link]) of the target resource. + /// IResourceLinkObjectPropertiesSelectionStage WithProfileUri(string profile); /// @@ -93,6 +101,7 @@ public interface IResourceLinkObjectPropertiesSelectionStage : IResourceLinkCrea /// /// Its value is a string and is intended for labelling the link with a /// human-readable identifier(as defined by [RFC5988]). + /// IResourceLinkObjectPropertiesSelectionStage WithTitle(string title); /// @@ -107,6 +116,13 @@ public interface IResourceLinkObjectPropertiesSelectionStage : IResourceLinkCrea /// /// Its value is a string and is intended for indicating the language of /// the target resource(as defined by [RFC5988]). + /// IResourceLinkObjectPropertiesSelectionStage WithHreflang(string hreflang); + /// + /// Forces the link relation being built to serialize as a JSON array even when it contains a + /// single . No-op when the relation is "curies", which is array-form + /// by default since 2.0.0 per HAL section 8.3 + /// + /// A to continue building the new IResourceLinkObjectPropertiesSelectionStage AsArray(); } diff --git a/src/Chatter.Rest.Hal/Chatter.Rest.Hal.csproj b/src/Chatter.Rest.Hal/Chatter.Rest.Hal.csproj index be507e1..fd856e2 100644 --- a/src/Chatter.Rest.Hal/Chatter.Rest.Hal.csproj +++ b/src/Chatter.Rest.Hal/Chatter.Rest.Hal.csproj @@ -4,7 +4,7 @@ net8.0;netstandard2.0 10.0 enable - 2.1.0 + 2.1.1 Brennan Pike Brennan Pike A dotnet/c# implementation of the Hypertext Application Language (HAL+json) specification for RESTful Web Apis @@ -14,6 +14,7 @@ .net;Chatter;REST;HAL;hal+json;HATEOAS MIT true + true true diff --git a/src/Chatter.Rest.Hal/Converters/ConverterHelpers.cs b/src/Chatter.Rest.Hal/Converters/ConverterHelpers.cs index 52cef61..0c4fc5c 100644 --- a/src/Chatter.Rest.Hal/Converters/ConverterHelpers.cs +++ b/src/Chatter.Rest.Hal/Converters/ConverterHelpers.cs @@ -26,8 +26,9 @@ internal static class ConverterHelpers /// True when the caller registered a custom converter for in /// . Options-registered converters take precedence /// over attribute-wired ones, so the node-walking fast path is only valid when the selected - /// converter is the built-in one; otherwise the nested part must dispatch through - /// so the custom converter runs. + /// converter is the built-in one; otherwise the nested part must dispatch through the + /// extension + /// so the custom converter runs. /// internal static bool HasCustomConverter(JsonSerializerOptions options, Type builtInConverterType) => options.GetConverter(typeof(T))?.GetType() != builtInConverterType; diff --git a/src/Chatter.Rest.Hal/Converters/ResourceConverter.cs b/src/Chatter.Rest.Hal/Converters/ResourceConverter.cs index 34948c6..f54ab1b 100644 --- a/src/Chatter.Rest.Hal/Converters/ResourceConverter.cs +++ b/src/Chatter.Rest.Hal/Converters/ResourceConverter.cs @@ -39,7 +39,7 @@ public sealed class ResourceConverter : JsonConverter /// /// Materializes a Resource directly from an already-parsed node. Nested converters call this - /// instead of , which would re-serialize the subtree to UTF-8 + /// instead of , which would re-serialize the subtree to UTF-8 /// and re-parse it — repeating that once per ancestor turns a deeply nested _embedded /// chain into O(depth × size) work; walking the existing tree keeps the whole document at O(size). /// diff --git a/src/Chatter.Rest.Hal/Resource.cs b/src/Chatter.Rest.Hal/Resource.cs index 0e5fce9..230bc41 100644 --- a/src/Chatter.Rest.Hal/Resource.cs +++ b/src/Chatter.Rest.Hal/Resource.cs @@ -72,10 +72,10 @@ internal object? StateObject } /// - /// Gets the cached state object, bypassing the Link-guard logic in . + /// Gets the cached state object, bypassing the Link-guard logic in . /// /// - /// contains defensive checks that prevent a -typed + /// contains defensive checks that prevent a -typed /// state from being misinterpreted as a object during deserialization. /// This property skips those checks and is therefore only safe for the serialization (write) path, /// where the state is being written out rather than interpreted as a domain type.