From a962f917b111358332ed38130b16f9d1f9299457 Mon Sep 17 00:00:00 2001 From: Mathias Reichardt Date: Wed, 4 Mar 2026 20:53:54 +0100 Subject: [PATCH 01/10] Fix typos and out dated --- content/01-Key-concepts.md | 2 +- content/03-HypermediaObject.md | 2 +- content/04-Entity-and-Links.md | 4 ++-- content/05-Endpoints.md | 4 ++-- content/06-Url-key-extraction.md | 5 +++-- content/07-Route-design.md | 2 +- content/09-Release-notes.md | 2 +- content/10-Serialization.md | 2 +- content/12-Notes-on-Siren.md | 6 ++++-- content/13-Dynamic-content.md | 4 ++-- 10 files changed, 18 insertions(+), 15 deletions(-) diff --git a/content/01-Key-concepts.md b/content/01-Key-concepts.md index 7a034a5..609bb5d 100644 --- a/content/01-Key-concepts.md +++ b/content/01-Key-concepts.md @@ -6,7 +6,7 @@ nav_order: 1 # Key concepts -## Server side APS.NET +## Server side ASP.NET RESTyard allows you to build a restful web server which responds with Siren documents without building a Siren class and assigning URIs to Links and embedded Entities. For this RESTyard provides two main components: the `HypermediaObject` class and new RouteAttributes extending the Web API RouteAttributes. diff --git a/content/03-HypermediaObject.md b/content/03-HypermediaObject.md index 7bed39d..d515f1e 100644 --- a/content/03-HypermediaObject.md +++ b/content/03-HypermediaObject.md @@ -24,7 +24,7 @@ public class HypermediaCustomer : HypermediaObject [HypermediaAction(Title = "Marks a Customer as a favorite buyer.")] public HypermediaActionCustomerMarkAsFavorite MarkAsFavoriteAction { get; private set; } - // Hides the Property so it will not be pressent in the Hypermedia. Onyl on top level + // Hides the Property so it will not be present in the Hypermedia. Only on top level [FormatterIgnoreHypermediaProperty] public int Id { get; set; } diff --git a/content/04-Entity-and-Links.md b/content/04-Entity-and-Links.md index fa8aa47..e1019b5 100644 --- a/content/04-Entity-and-Links.md +++ b/content/04-Entity-and-Links.md @@ -24,13 +24,13 @@ References to other `HypermediaObjects` are represented by references which deri Use a `HypermediaObjectReference` to create a reference. This reference can then be added to the Links dictionary with an associated relation: -```cshap +```csharp Links.Add("NiceCar", new HypermediaObjectReference(new HypermediaCar("VW", 2))); ``` or the Entities list (which can contain duplicates): -```cshap +```csharp Entities.Add("NiceCar", new HypermediaObjectReference(new HypermediaCar("VW", 2))); ``` diff --git a/content/05-Endpoints.md b/content/05-Endpoints.md index 0dbc27d..ebff9e3 100644 --- a/content/05-Endpoints.md +++ b/content/05-Endpoints.md @@ -42,7 +42,7 @@ public ActionResult GetRootDocument() The same goes for Actions: ```csharp -[HttpPostHypermediaAction("CreateCustomer", typeof(HypermediaFunction>))] +[HttpPostHypermediaAction("CreateCustomer", typeof(HypermediaAction))] public async Task NewCustomerAction([SingleParameterBinder(typeof(CreateCustomerParameters))] CreateCustomerParameters createCustomerParameters) { if (createCustomerParameters == null) @@ -153,7 +153,7 @@ public class UploadCarImageOp : FileUploadHypermediaAction Files are uploaded using `multipart/form-data`. The additional parameter is added as a serialized json string to the key-value-dictionary of the form. -c\# client example: +C# client example: ```csharp // hco definition diff --git a/content/06-Url-key-extraction.md b/content/06-Url-key-extraction.md index 0b04a66..889a404 100644 --- a/content/06-Url-key-extraction.md +++ b/content/06-Url-key-extraction.md @@ -21,7 +21,7 @@ public interface IKeyFromUriService ``` This service will, given the `Uri`, the type of the HypermediaObject, extract all the properties defined in `TKey` from the `Uri` and return a `Result`. -Not that since this results a `Result<>` object, no Exceptions will be thrown, and all error cases are handled by returning a `Result.Error` case. +Note that since this returns a `Result<>` object, no Exceptions will be thrown, and all error cases are handled by returning a `Result.Error` case. ## Example @@ -116,6 +116,7 @@ public class FavoriteCustomer : IHypermediaActionParameter [KeyFromUri(typeof(HypermediaCustomer), schemaProperyName: "Customers")] public List CustomerId { get; set; } } +``` The post would look like: @@ -150,7 +151,7 @@ public class Parameter : IHypermediaActionParameter } ``` -Not two properties have an attribute indicating that they should be filled: `Brand` and `CarId`. Both share the same type of resource and `schemaProperyName` because the source of their value is a single URL in the JSON payload. +Note: two properties have an attribute indicating that they should be filled: `Brand` and `CarId`. Both share the same type of resource and `schemaProperyName` because the source of their value is a single URL in the JSON payload. To configure which route template variable (see your attributed route) should be used to fill the parameter property `routeTemplateParameterName` is used. The route template: `{brand}/{key:int}`. Be careful to match the variable name and `routeTemplateParameterName`. diff --git a/content/07-Route-design.md b/content/07-Route-design.md index ba66a94..afd05b0 100644 --- a/content/07-Route-design.md +++ b/content/07-Route-design.md @@ -24,7 +24,7 @@ http://localhost:5000/Customers/CreateQuery http://localhost:5000/Customers/CreateCustomer ``` -- Entities are accessed through a collection but do not host child Entities. These should be handled in their own collections. The routes to the actual objects should not matter, so no need to nest them. This helps to flatten the Controller hierarchy and avoids deep routes. If a placeholder variable is required in the route template name it _key_ (see Known Issues below). +- Entities are accessed through a collection but do not host child Entities. These should be handled in their own collections. The routes to the actual objects should not matter, so no need to nest them. This helps to flatten the Controller hierarchy and avoids deep routes. If a placeholder variable is required in the route template name it _key_. Examples ```html diff --git a/content/09-Release-notes.md b/content/09-Release-notes.md index 11d7483..969c8ad 100644 --- a/content/09-Release-notes.md +++ b/content/09-Release-notes.md @@ -1,7 +1,7 @@ --- layout: default title: Release Notes -nav_order: 10 +nav_order: 13 --- # Release Notes diff --git a/content/10-Serialization.md b/content/10-Serialization.md index f80c1c9..ba6d68d 100644 --- a/content/10-Serialization.md +++ b/content/10-Serialization.md @@ -8,7 +8,7 @@ nav_order: 9 - Enums in `HypermediaObjects` and Action parameters can be attributed using `EnumMember` to specify fixed string names for enum values. If no attribute is present the value will be serialized using `ToString()` - `DateTime` and `DateTimeOffset` will be serialized using ISO 8601 notation: e.g. `2000-11-22T18:05:32.9990000+02:00` -- When properties contain classes the follwoing attributes are not applied: +- When properties contain classes the following attributes are not applied: - `FormatterIgnoreHypermediaPropertyAttribute` - `HypermediaActionAttribute` - `HypermediaObjectAttribute` diff --git a/content/12-Notes-on-Siren.md b/content/12-Notes-on-Siren.md index 13eb0de..5fa71db 100644 --- a/content/12-Notes-on-Siren.md +++ b/content/12-Notes-on-Siren.md @@ -108,10 +108,12 @@ Example: { "name": "UploadFiles", "type": "file", - "accept": ".png, image/*'", + "accept": ".png, image/*", "maxFileSizeBytes": 209715200, "allowMultiple": false } ] - }, + } + ] +} ``` diff --git a/content/13-Dynamic-content.md b/content/13-Dynamic-content.md index 3da2f5a..13743fe 100644 --- a/content/13-Dynamic-content.md +++ b/content/13-Dynamic-content.md @@ -6,7 +6,7 @@ nav_order: 12 # Dynamic content -Although the framework is build on the intend of providing type information as much as possible it can be necessary to deviate from this approach. +Although the framework is built with the intent of providing type information as much as possible it can be necessary to deviate from this approach. ## Dynamic Actions {: .d-inline-block } @@ -28,7 +28,7 @@ Prefilled values can be provided as object which will be serialized and as strin Example: ```csharp -// Action route, retreiving raw json +// Action route, retrieving raw json [HttpPostHypermediaAction("/MyDynamicAction", typeof(MyDynamicOp))] public ActionResult MyDynamicAction([FromBody] JsonElement rawObject) { From 29e13a88052b647cfa9ac4c137a94cf5101e93a6 Mon Sep 17 00:00:00 2001 From: Mathias Reichardt Date: Fri, 6 Mar 2026 00:00:00 +0100 Subject: [PATCH 02/10] Update usage for links embedded netities and attibutes --- content/03-HypermediaObject.md | 104 ++++++++++----- content/04-Entity-and-Links.md | 121 ++++++++++------- content/05-Endpoints.md | 233 ++++++++++++++++++++------------- 3 files changed, 280 insertions(+), 178 deletions(-) diff --git a/content/03-HypermediaObject.md b/content/03-HypermediaObject.md index d515f1e..89ec465 100644 --- a/content/03-HypermediaObject.md +++ b/content/03-HypermediaObject.md @@ -6,69 +6,101 @@ nav_order: 3 # HypermediaObject -This is the base class for all entities (in Siren format) which shall be returned from the server. Derived types from HypermediaObjects can be thought of as kind of a DTO (Data Transfer Object). A fitting name would be HTO, Hypermedia Transfer Object. They accumulate all information which should be present in the formatted Hypermedia document and will be formatted as Siren Hypermedia by the included formatter. -An Example from the demo project CarShack: +A HypermediaObject (HTO, Hypermedia Transfer Object) represents an entity that will be serialized as a Siren document. HTOs implement the `IHypermediaObject` marker interface and accumulate all information which should be present in the formatted Hypermedia document. + +An example from the demo project CarShack: ```csharp [HypermediaObject(Title = "A Customer", Classes = new[] { "Customer" })] -public class HypermediaCustomer : HypermediaObject +public class HypermediaCustomerHto : IHypermediaObject { - private readonly Customer customer; - - // Add actions: - // Each ActionType must be unique and a corresponding route must exist so the formatter can look it up. - // See the CustomerController. - [HypermediaAction(Name = "CustomerMove", Title = "A Customer moved to a new location.")] - public HypermediaActionCustomerMoveAction MoveAction { get; private set; } - - [HypermediaAction(Title = "Marks a Customer as a favorite buyer.")] - public HypermediaActionCustomerMarkAsFavorite MarkAsFavoriteAction { get; private set; } - - // Hides the Property so it will not be present in the Hypermedia. Only on top level + // Hidden from Siren output, used only for route key resolution + [Key("id")] [FormatterIgnoreHypermediaProperty] public int Id { get; set; } // Assigns an alternative name, so this stays constant even if property is renamed [HypermediaProperty(Name = "FullName")] - public string Name { get; set; } + public string? Name { get; set; } + public int? Age { get; set; } + public AddressTo? Address { get; set; } + public bool IsFavorite { get; set; } - public int Age { get; set; } + // Typed link — the framework resolves the URL automatically + [Relations(["PurchaseHistory"])] + public ILink PurchaseHistory { get; set; } - public string Address { get; set; } + // Self link + [Relations([DefaultHypermediaRelations.Self])] + public ILink Self { get; set; } - public bool IsFavorite { get; set; } + // Actions — only included in Siren output if CanExecute() returns true + [HypermediaAction(Name = "CustomerMove", Title = "A Customer moved to a new location.")] + public CustomerMoveOp CustomerMove { get; set; } + + [HypermediaAction(Name = "MarkAsFavorite", Title = "Marks a Customer as a favorite buyer.")] + public MarkAsFavoriteOp MarkAsFavorite { get; set; } - public HypermediaCustomer(Customer customer) + public HypermediaCustomerHto(int id, string? name, int? age, AddressTo? address, bool isFavorite, + CustomerMoveOp customerMove, MarkAsFavoriteOp markAsFavorite) { - this.customer = customer; + Id = id; + Name = name; + Age = age; + Address = address; + IsFavorite = isFavorite; + CustomerMove = customerMove; + MarkAsFavorite = markAsFavorite; + PurchaseHistory = Link.ByQuery( + new CustomerPurchaseHistoryQuery(), new CustomerPurchaseHistoryHto.Key(id)); + Self = Link.To(this); + } + + // Action types — derive from HypermediaAction or HypermediaAction (parameter-less) + public class CustomerMoveOp : HypermediaAction + { + public CustomerMoveOp(Func canExecute) : base(canExecute) { } + } - Name = customer.Name; - ... + public class MarkAsFavoriteOp : HypermediaAction + { + public MarkAsFavoriteOp(Func canExecute) : base(canExecute) { } + } - MoveAction = new HypermediaActionCustomerMoveAction(CanMove, DoMove); - ... + // Key record for route resolution (see Endpoints page) + public record Key(int Id) : HypermediaObjectKeyBase + { + protected override IEnumerable> EnumerateKeysForLinkGeneration() + { + yield return new KeyValuePair("id", this.Id); + } } -... } +// Action parameters are records implementing IHypermediaActionParameter +public record NewAddress(AddressTo Address) : IHypermediaActionParameter; +public record AddressTo(string Street, string Number, string City, string ZipCode); +public record MarkAsFavoriteParameters(Uri Customer) : IHypermediaActionParameter; ``` **In short:** -- Public Properties will be formatted to Siren Properties. -- No Properties which hold a class will be serialized -- By default Properties which are null will not be added to the Siren document. -- It is recommended to represented optional values as `Nullable` -- Properties with a `HypermediaActionBase` type will be added as Actions, but only if CanExecute returns true. Any required parameters will be added in the "fields" section of the Siren document. -- Other `HypermediaObject`s can be embedded by adding them as a `HypermediaObjectReferenceBase` type to the entities collection Property (not shown in this example, see HypermediaCustomerQueryResult in the demo project). -- Links to other `HypermediaObject`s can be added to the Links collection Property, also as `HypermediaObjectReferenceBase` (not shown in this example, see HypermediaCustomersRoot in the demo project). -- Properties, Actions and `HypermediaObject`s themselves can be attributed e.g. to give them a fixed name: +- HTOs implement `IHypermediaObject` and are decorated with `[HypermediaObject(Title = "...", Classes = [...])]` +- Public properties will be formatted to Siren properties +- Properties which hold a class are recursively serialized (their public properties become nested JSON objects). Note that HTO-specific attributes like `[FormatterIgnoreHypermediaProperty]` are only applied on top-level properties, not inside nested classes. +- By default properties which are null will not be added to the Siren document +- It is recommended to represent optional values as `Nullable` +- Links to other HTOs are typed `ILink` properties decorated with `[Relations(["..."])]` — see [Embedded Entities and Links]({% link content/04-Entity-and-Links.md %}) +- Embedded entities use `List>` populated with `EmbeddedEntity.Embed()` — see [Embedded Entities and Links]({% link content/04-Entity-and-Links.md %}) +- Properties with a `HypermediaActionBase` type will be added as Actions, but only if `CanExecute()` returns true. Action types derive from `HypermediaAction` (with parameter) or `HypermediaAction` (parameter-less). Required parameters will be added in the "fields" section of the Siren document. +- Action parameters implement `IHypermediaActionParameter` (records are recommended) +- Properties, Actions and HTOs themselves can be attributed e.g. to give them a fixed name: - `FormatterIgnoreHypermediaPropertyAttribute` - `HypermediaActionAttribute` - `HypermediaObjectAttribute` - `HypermediaPropertyAttribute` - + **Note:** This is only done for top level properties since the object tree is not traversed. {: .highlight } -All `HypermediaObject`'s used in a Link or as embedded Entity and all `HypermediaAction`'s in a `HypermediaObject` require that there is an attributed route for their Type. Otherwise the formatter is not able to resolve the URI and will throw an Exception. +All HTOs used in a Link or as embedded Entity and all Actions in an HTO require that there is an attributed route for their Type. Otherwise the formatter is not able to resolve the URI and will throw an Exception. diff --git a/content/04-Entity-and-Links.md b/content/04-Entity-and-Links.md index e1019b5..1ce88a8 100644 --- a/content/04-Entity-and-Links.md +++ b/content/04-Entity-and-Links.md @@ -18,86 +18,109 @@ nav_order: 4 --- -References to other `HypermediaObjects` are represented by references which derive from `HypermediaObjectReferenceBase`. These references are the added to the `Links` list or the `Entities` list of a `HypermediaObject`. +Links and embedded entities are declared as typed properties on your HTO class, decorated with `[Relations(["..."])]`. -## Option 1: If a instance of the referenced HypermediaObject is available +## Links -Use a `HypermediaObjectReference` to create a reference. This reference can then be added to the Links dictionary with an associated relation: +### Option 1: If an instance of the referenced HTO is available + +Use `Link.To()` to create a link from an existing instance: ```csharp -Links.Add("NiceCar", new HypermediaObjectReference(new HypermediaCar("VW", 2))); +[Relations(["NiceCar"])] +public ILink NiceCar { get; set; } + +// In constructor +NiceCar = Link.To(carInstance); ``` -or the Entities list (which can contain duplicates): +### Option 2: If no instance is available (key reference) + +Use `Link.ByKey()` to reference an HTO by its key without instantiating it: ```csharp -Entities.Add("NiceCar", new HypermediaObjectReference(new HypermediaCar("VW", 2))); -``` +[Relations(["BestCustomer"])] +public ILink BestCustomer { get; set; } -{: .highlight } -The used function is an convenience extension contained in `RESTyard.AspNetCore.Hypermedia.Extensions` +// In constructor — reference by key only +BestCustomer = Link.ByKey(new HypermediaCustomerHto.Key(1)); +``` -## Option 2: If no instance is available or not necessary +The framework will resolve the URL using the key and the route registered for `HypermediaCustomerHto`. The `Key` record must derive from `HypermediaObjectKeyBase` and provide the route template values. For simple cases, you can use `[Key]` attributes on HTO properties instead (see [Endpoints]({% link content/05-Endpoints.md %})). -To allow referencing of HypermediaObjects without the need to instantiate them, for reference purpose only, there are two additional references available. +### Option 3: If a query result should be referenced -use a `HypermediaObjectKeyReference` if the object requires a key to be identified e.g. the Customers id. +Use `Link.ByQuery()` to reference a query result HTO. The query object is serialized into the URL's query string: ```csharp -Links.Add("BestCustomer", new HypermediaObjectKeyReference(typeof(HypermediaCustomer), 1)); +[Relations(["all"])] +public ILink All { get; set; } + +// In constructor +All = Link.ByQuery(allQuery); ``` -The reference requires the type of the referenced HypermediaObject, here `HypermediaCustomer` and a key which is used by the related route to identify the desired entity. The framework will pass the key object to the `KeyProducer` instance which is assigned to the HypermediaObject's route, here `CustomerRouteKeyProducer`. Explicit assignment of RouteKeyProducers is optional. `KeyAttribute` can be used alternatively on key properties of the HypermediaObject. For more details on attributed routes see [Attributed routes](## Attributed routes). +### Optional links -Example from the CarShack demo project `CustomerController.cs` +Links can be nullable to indicate they are conditionally present. You can use `Option` from FunicularSwitch to map: ```csharp -[HttpGetHypermediaObject("{key:int}", typeof(HypermediaCustomer), typeof(CustomerRouteKeyProducer))] -public async Task GetEntity(int key) -{ -.. - var customer = await customerRepository.GetEnitityByKeyAsync(key); - var result = new HypermediaCustomer(customer); - return Ok(result); -... -} +[Relations(["OkaySite"])] +public ExternalLink? OkaySite { get; set; } + +// In constructor — conditionally present +OkaySite = okaySite.Map(some => Link.External(some)).GetValueOrDefault(); ``` -The `CustomerRouteKeyProducer` is responsible for the translation of the domain specific key `object` to a key which is usable in the route context. It must be an anonymous object where all properties match the rout template parameters, here `{key:int}`. +## Embedded Entities -```csharp -public object CreateFromKeyObject(object keyObject) -{ - return new { key = keyObject }; -} -``` +Use `List>` to embed a list of HTOs. Populate with `EmbeddedEntity.Embed()`: -## Option 3: If a query result should be referenced +```csharp +[Relations(["Customers"])] +public List> Customers { get; set; } -Use a `HypermediaObjectQueryReference` if the object requires also query object `IHypermediaQuery` to be created e.g. a result object which contains several Customers. -For a reference to a query result: `HypermediaQueryResult` it is also required to provide the query to the reference, so the link to the object can be constructed. +// In constructor +Customers = customerList + .Select(c => EmbeddedEntity.Embed(c)) + .ToList(); +``` -Example from `HypermediaCustomersRoot.cs`: +A single embedded entity can be nullable. If `null`, it will be omitted from the Siren output: ```csharp -var allQuery = new CustomerQuery(); -Links.Add(DefaultHypermediaRelations.Queries.All, new HypermediaObjectQueryReference(typeof(HypermediaCustomerQueryResult), allQuery)); +[Relations(["FeaturedItem"])] +public IEmbeddedEntity? FeaturedItem { get; set; } + +// In constructor — conditionally present +FeaturedItem = featuredCar is not null + ? EmbeddedEntity.Embed(featuredCar) + : null; ``` -## Direct References +## External and Internal References -It might be necessary to reference a external source or a route which can not be build by the framework. In this case use the `ExternalReference` for links outside of the server or `InternalReference` for server routes. These objects work around the default route resolving process by providing its own URI or route name. It can only be used in combination with `HypermediaObjectReference`. -As additional information for clients a external reference can contain a media type or a list of media types. This is useful if a client wants to switch the media type e.g. to a download or get the resource as image. +For links to sources outside the server or routes that cannot be built by the framework, use `ExternalReference` or `InternalReference` with `Link.External()`. + +```csharp +[Relations(["GreatSite"])] +public ExternalLink GreatSite { get; set; } + +// In constructor +GreatSite = Link.External(new HypermediaObjectReference( + new ExternalReference(new Uri("https://www.example.com/")) + .WithAvailableMediaType("text/html"))); +``` -Example references of an external site: +An external reference can contain a media type or a list of media types. This is useful if a client wants to switch the media type e.g. to a download or get the resource as an image. ```csharp -Links.Add("GreatSite", new ExternalReference(new Uri("http://www.example.com/"))); -Links.Add("GreatSite", new ExternalReference(new Uri("http://www.example.com/")).WithAvailableMediaType("image/png")); -Links.Add("GreatSite", new ExternalReference(new Uri("http://www.example.com/")).WithAvailableMediaTypes(new []{"application/xml", "image/png"})); - -Links.Add("GreatSite", new InternalReference("My_Route_Name")); -Links.Add("GreatSite", new InternalReference("My_Route_Name", new {routevariable1 = 1})); -Links.Add("GreatSite", new InternalReference("My_Route_Name").WithAvailableMediaType("image/png")); -Links.Add("GreatSite", new InternalReference("My_Route_Name").WithAvailableMediaTypes(new []{"application/xml", "image/png"})); +// External references with media types +new ExternalReference(new Uri("https://www.example.com/")).WithAvailableMediaType("image/png") +new ExternalReference(new Uri("https://www.example.com/")).WithAvailableMediaTypes(["application/xml", "image/png"]) + +// Internal references (by route name) for server routes that can't be built by the framework +new InternalReference("My_Route_Name") +new InternalReference("My_Route_Name", new { routevariable1 = 1 }) +new InternalReference("My_Route_Name").WithAvailableMediaType("image/png") ``` diff --git a/content/05-Endpoints.md b/content/05-Endpoints.md index ebff9e3..59186b3 100644 --- a/content/05-Endpoints.md +++ b/content/05-Endpoints.md @@ -18,42 +18,43 @@ nav_order: 5 --- -The included SirenFormatter will build required links to other routes. At startup all routes attributed with: +{: .note } +If you're migrating from an older version, the legacy attributes (`HttpGetHypermediaObject`, `HttpPostHypermediaAction`, etc.) are still supported but deprecated. The included Roslyn analyzers (RY0010–RY0015) will suggest migration to the new attributes. -- `HttpGetHypermediaObject` -- `HttpPostHypermediaAction` -- `HttpDeleteHypermediaAction` -- `HttpPatchHypermediaAction` -- `HttpGetHypermediaActionParameterInfo` +The included SirenFormatter will build required links to other routes. At startup all routes attributed with the following will be placed in an internal register: -will be placed in an internal register. +- `HypermediaObjectEndpoint` — for GET routes returning HTOs +- `HypermediaActionEndpoint(nameof(...))` — for POST/PUT/PATCH/DELETE action routes +- `HypermediaActionParameterInfoEndpoint` — for action parameter schema routes -This means that for every `HypermediaObject` there must be a route with matching type. -Example from the demo project CustomerRootController: +This means that for every HTO there must be a route with matching type. -``` csharp -[HttpGetHypermediaObject("", typeof(HypermediaCustomersRoot))] +Example from the demo project CustomersRootController: + +```csharp +[HttpGet(""), HypermediaObjectEndpoint] public ActionResult GetRootDocument() { return Ok(customersRoot); } ``` -The same goes for Actions: +The same goes for Actions. The action endpoint references the HTO that owns the action and names the action property: ```csharp -[HttpPostHypermediaAction("CreateCustomer", typeof(HypermediaAction))] -public async Task NewCustomerAction([SingleParameterBinder(typeof(CreateCustomerParameters))] CreateCustomerParameters createCustomerParameters) +[HttpPost("CreateCustomer"), + HypermediaActionEndpoint(nameof(HypermediaCustomersRootHto.CreateCustomer))] +public async Task NewCustomerAction(CreateCustomerParameters createCustomerParameters) { if (createCustomerParameters == null) { return this.Problem(ProblemJsonBuilder.CreateBadParameters()); } - var createdCustomer = await customersRoot.CreateCustomerAction.Execute(createCustomerParameters); + var createdCustomer = await CreateCustomer(createCustomerParameters); // Will create a Location header with a URI to the result. - return this.Created(new HypermediaCustomer(createdCustomer)); + return this.Created(Link.To(createdCustomer)); } ``` @@ -63,7 +64,7 @@ Siren specifies that to trigger an action an array of parameters should be poste A valid JSON for this route would look like this: ```json -[{"CreateCustomerParameters": +[{"CreateCustomerParameters": { "Name":"Hans Schmid" } @@ -73,7 +74,7 @@ A valid JSON for this route would look like this: The parameter binder also allows to pass a parameter object without the wrapping array: ```json -{"CreateCustomerParameters": +{"CreateCustomerParameters": { "Name":"Hans Schmid" } @@ -83,24 +84,25 @@ The parameter binder also allows to pass a parameter object without the wrapping Parameters for actions may define a route which provides additional type information to the client. These routes will be added to the Siren fields object as "class". ```csharp -[HttpGetHypermediaActionParameterInfo("CreateCustomerParametersType", typeof(CreateCustomerParameters))] -public ActionResult CreateCustomerParametersType() +[HttpGet("NewAddressType"), HypermediaActionParameterInfoEndpoint] +public ActionResult NewAddressType() { - var schema = JsonSchemaFactory.Generate(typeof(CreateCustomerParameters)); + var schema = jsonSchemaFactory.Generate(typeof(NewAddress)); return Ok(schema); } ``` -Also see See: [URL key extraction]({% link content/06-Url-key-extraction.md %}) +Also see: [URL key extraction]({% link content/06-Url-key-extraction.md %}) ## Actions with prefilled values -Actions supply contain prefilled values so a form is already filled with server provided content. Actions with parameters have a optional parameter: +Actions can supply prefilled values so a form is already filled with server provided content. Actions with parameters have an optional parameter: ```csharp -public class ActionWithArgument : HypermediaAction +public class CreateQueryOp : HypermediaAction { - public ActionWithArgument(Func canExecute, ActionParameter prefilledValues) : base(canExecute, prefilledValues) + public CreateQueryOp(Func canExecute, CustomerQuery? prefilledValues = default) + : base(canExecute, prefilledValues) { } } @@ -108,18 +110,14 @@ public class ActionWithArgument : HypermediaAction ## Actions with acceptable media type -The action attributes allow to specify a media type so it can be transmitted that a client should send the data in a acceptable format - -- `HttpPostHypermediaAction` -- `HttpDeleteHypermediaAction` -- `HttpPatchHypermediaAction` -- `HttpPutHypermediaAction` +The `HypermediaActionEndpoint` attribute is combined with standard HTTP method attributes. To specify an acceptable media type, use the `DefaultMediaTypes` constants: ```csharp -[HttpPostHypermediaAction("my/route/template", typeof(MyOperation), AcceptedMediaType = "multipart/form-data"))] +[HttpPost("UploadImage"), + HypermediaActionEndpoint(nameof(HypermediaCarsRootHto.UploadCarImage), DefaultMediaTypes.MultipartFormData)] ``` -Will be rendered to siren as `type` on the action. Default is `application/json`. +Will be rendered to Siren as `type` on the action. Default is `application/json`. ## File upload actions @@ -129,23 +127,22 @@ Controller example: ```csharp // controller -[HttpPostHypermediaAction( - "UploadImage", - typeof(HypermediaCarsRootHto.UploadCarImageOp), - AcceptedMediaType = DefaultMediaTypes.MultipartFormData)] +[HttpPost("UploadImage"), + HypermediaActionEndpoint(nameof(HypermediaCarsRootHto.UploadCarImage), DefaultMediaTypes.MultipartFormData)] public async Task UploadCarImage( [HypermediaUploadParameterFromForm] HypermediaFileUploadActionParameter uploadParameters) { var files = uploadParameters.Files; // Access uploaded files - var additionalParameter = uploadParameters.ParameterObject // Access generic additional parameter + var additionalParameter = uploadParameters.ParameterObject; // Access generic additional parameter //... } // action definition -public class UploadCarImageOp : FileUploadHypermediaAction +public class UploadCarImageOp : FileUploadHypermediaAction { - public UploadCarImageOp(Func canExecute, FileUploadConfiguration fileUploadConfiguration = null) : base(canExecute, fileUploadConfiguration) + public UploadCarImageOp(Func canExecute, FileUploadConfiguration? fileUploadConfiguration = null) + : base(canExecute, fileUploadConfiguration) { } } @@ -175,40 +172,52 @@ hco.UploadCarImage.ExecuteAsync( Resolver); ``` -note that the name and filename are mandatory in order for the file to be recognized as a file and not as a "normal" parameter. +Note that the name and filename are mandatory in order for the file to be recognized as a file and not as a "normal" parameter. ## Calling external APIs using Actions -If it is necessary to call a external API and expose that call as an action there is `HypermediaExternalAction` and `HypermediaExternalAction` to be used as base for ActionTypes. +If it is necessary to call an external API and expose that call as an action there is `HypermediaExternalAction` and `HypermediaExternalAction` to be used as base for action types. ```csharp -public class ExternalActionNoParameters :HypermediaExternalAction +public class ExternalActionNoParameters : HypermediaExternalAction { - public ExternalActionNoParameters(Uri externalUri, HttpMethod httpMethod) + public ExternalActionNoParameters(Uri externalUri, string httpMethod) : base(() => true, externalUri, httpMethod) { } } -public class ExternalActionWithParameter : HypermediaExternalAction +public class ExternalActionWithArgument : HypermediaExternalAction +{ + public ExternalActionWithArgument( + Uri externalUri, + string httpMethod, + string acceptedMediaType, + ExternalActionParameters prefilledValues) + : base(() => true, externalUri, httpMethod, acceptedMediaType, prefilledValues) { } +} + +public class ExternalActionParameters : IHypermediaActionParameter { - public ExternalActionWithParameter(Uri externalUri, - HttpMethod httpMethod) - : base(() => true, - externalUri, - httpMethod, - "myCustom/mediaType", - new ExternalActionParameters(3)) { } + public int AInt { get; } + public ExternalActionParameters(int aInt) { AInt = aInt; } } // usage in HTO: -public ExternalActionNoParameters ExternalActionNoParametersNoParametersTest { get; init; } = new ExternalActionNoParameters(new Uri("http://www.example1.com"), HttpMethod.POST); -public ExternalActionWitParameter ExternalActionWitParameterTestOp { get; init; }= new ExternalActionWitParameter(new Uri("http://www.example2.com"), HttpMethod.DELETE); +public ExternalActionNoParameters ExternalAction { get; init; } + = new ExternalActionNoParameters(new Uri("http://www.example.com"), HttpMethod.POST); + +public ExternalActionWithArgument ExternalActionWithArgs { get; init; } + = new ExternalActionWithArgument( + new Uri("http://www.example2.com"), + HttpMethod.DELETE, + "application/json", + new ExternalActionParameters(3)); ``` ## Routes with a placeholder in the route template For access to entities a route template may contain placeholder variables like _key_ in the example below. -If a `HypermediaObject` is referenced, e.g. the self link or a link to another Customer, the formatter must be able to create the URI to the linked `HypermediaObject`. -To properly fill the placeholder variables for such routes a `KeyProducer` is required. +If an HTO is referenced, e.g. the self link or a link to another Customer, the formatter must be able to create the URI to the linked HTO. +To properly fill the placeholder variables for such routes, the framework needs to know how to extract key values from the HTO. ### Use attributes to indicate keys @@ -216,65 +225,87 @@ Use the `Key` attribute to indicate which properties of the HTO should be used t If there is only one variable to fill it is enough to put the attribute above the desired HTO property. {: .highlight } -A `HttpGetHypermediaObject` route must exists for the resolution to be added. +A `HypermediaObjectEndpoint` route must exist for the resolution to be added. Example: -The route template: `[HttpGetHypermediaObject("{key:int}", typeof(MyHypermediaObject))]` + +The route template: `[HttpGet("{key:int}"), HypermediaObjectEndpoint]` + The attributed HTO: ```csharp -public class MyHypermediaObject : HypermediaObject +public class MyHto : IHypermediaObject { [Key] public int Id { get; set; } -... + ... ``` If the route has more than one variable, the `Key` attribute receives the name of the related route template variable. Example: -The route template: `[HttpGetHypermediaObject("{brand}/{key:int}", typeof(HypermediaCar))]` + +The route template: `[HttpGet("{brand}/{key:int}"), HypermediaObjectEndpoint]` + The attributed HTO: ```csharp [HypermediaObject(Title = "A Car", Classes = new[] { "Car" })] -public class HypermediaCar : HypermediaObject +public class HypermediaCarHto : IHypermediaObject { - // Marks property as part of the objects key so it is can be mapped to route parameters when creating links. + // Marks property as part of the objects key so it can be mapped to route parameters when creating links. [Key("brand")] public string Brand { get; set; } - // Marks property as part of the objects key so it is can be mapped to route parameters when creating links + // Marks property as part of the objects key so it can be mapped to route parameters when creating links [Key("key")] public int Id { get; set; } - ... } ``` -### Use a custom `KeyProducer` +### Use a Key record -Us use a custom `KeyProducer` implement IKeyProducer and add it to the attributed routes: -`[HttpGetHypermediaObject("{key:int}", typeof(HypermediaCustomer), typeof(CustomerRouteKeyProducer))]` to tell the framework which `KeyProducer to use for the route. +Alternatively, define a nested `Key` record that derives from `HypermediaObjectKeyBase`: -The formatter will call the producer if he has a instance of the referenced -Object (e.g. from `HypermediaObjectReference.GetInstance()`) and passes it to the `IKeyProducer:CreateFromHypermediaObject()` function. -Otherwise it will call `IKeyProducer:CreateFromKeyObject()` and passes the object provided by `HypermediaObjectKeyReference:GetKey(IKeyProducer keyProducer)`. -The `KeyProducer` must return an anonymous object filled with a property for each placeholder variable to be filled in the `HypermediaObject`'s route, here _key_. +```csharp +public class HypermediaCarHto : IHypermediaObject +{ + public int? Id { get; set; } + public string? Brand { get; set; } -A `KeyProducer` is added directly to the Attributed route as a Type and will be instantiated once by the framework. -See `CustomerRouteKeyProducer` in the demo project for an example. + public record Key(int? Id, string? Brand) : HypermediaObjectKeyBase + { + protected override IEnumerable> EnumerateKeysForLinkGeneration() + { + yield return new KeyValuePair("id", this.Id); + yield return new KeyValuePair("brand", this.Brand); + } + } +} +``` -``` csharp -[HttpGetHypermediaObject("Customers/{key:int}", typeof(HypermediaCustomer), typeof(CustomerRouteKeyProducer))] +### Use a custom `KeyProducer` + +To use a custom `KeyProducer`, implement `IKeyProducer` and add it to the attributed route: + +```csharp +[HttpGet("{key:int}"), HypermediaObjectEndpoint(typeof(CustomerRouteKeyProducer))] public async Task GetEntity(int key) { ... } ``` -By design the Extension encourages routes to not have multiple keys in the route template. Also only routes to a `HypermediaObject` may have a key. Actions related to -a `HypermediaObject` must be available as a sub route to its corresponding object so required route template variables can be filled for the actions host `HypermediaObject`. +The formatter will call the producer if it has an instance of the referenced object (e.g. from `Link.To()`) and passes it to `IKeyProducer.CreateFromHypermediaObject()`. +Otherwise it will call `IKeyProducer.CreateFromKeyObject()` and passes the key provided by `Link.ByKey()`. +The `KeyProducer` must return an anonymous object filled with a property for each placeholder variable to be filled in the HTO's route. + +See `CustomerRouteKeyProducer` in the demo project for an example. + +By design the extension encourages routes to not have multiple keys in the route template. Also only routes to an HTO may have a key. Actions related to +an HTO must be available as a sub route to its corresponding object so required route template variables can be filled for the action's host HTO. + Example: ```http @@ -284,36 +315,52 @@ http://localhost:5000/Customers/{key}/Move ## Queries -Clients shall not build query strings. Instead they post a JSON object to a `HypermediaAction` and receive the URI to the desired query result in the `Location` header. +Clients shall not build query strings. Instead they post a JSON object to an action and receive the URI to the desired query result in the `Location` header. -``` csharp -[HttpPostHypermediaAction("CreateQuery", typeof(HypermediaAction))] -public ActionResult NewQueryAction([SingleParameterBinder(typeof(CustomerQuery))] CustomerQuery query) +```csharp +[HttpPost("Queries"), + HypermediaActionEndpoint(nameof(HypermediaCustomersRootHto.CreateQuery))] +public ActionResult NewQueryAction(CustomerQuery query) { - ... + if (query == null) + { + return this.Problem(ProblemJsonBuilder.CreateBadParameters()); + } + + if (!customersRoot.CreateQuery.CanExecute()) + { + return this.CanNotExecute(); + } + // Will create a Location header with a URI to the result. - return this.CreatedQuery(typeof(HypermediaCustomerQueryResult), query); + return this.Created(Link.ByQuery(query)); } ``` There must be a companion route which receives the query object and returns the query result: -``` csharp -[HttpGetHypermediaObject("Query", typeof(HypermediaCustomerQueryResult))] +```csharp +[HttpGet("Query"), HypermediaObjectEndpoint] public async Task Query([FromQuery] CustomerQuery query) { - ... - var queryResult = await customerRepository.QueryAsync(query); - var resultReferences = new List(); + var resultReferences = new List(); foreach (var customer in queryResult.Entities) { - resultReferences.Add(new HypermediaObjectReference(new HypermediaCustomer(customer))); + resultReferences.Add(customer.ToHto()); } - var navigationQuerys = NavigationQuerysBuilder.Build(query, queryResult); - var result = new HypermediaCustomerQueryResult(resultReferences, queryResult.TotalCountOfEnties, query, navigationQuerys); - + var queries = NavigationQuerysBuilder.Create(query, queryResult); + var result = new HypermediaCustomerQueryResultHto( + queryResult.TotalCountOfEnties, + resultReferences.Count, + resultReferences, + queries.next.Map(IHypermediaQuery (some) => some), + queries.previous.Map(IHypermediaQuery (some) => some), + queries.last.Map(IHypermediaQuery (some) => some), + queries.all.Map(IHypermediaQuery (some) => some), + query); + return Ok(result); } ``` From 4945aeba56c798bc83e155c2ea3d9677a2743989 Mon Sep 17 00:00:00 2001 From: Mathias Reichardt Date: Fri, 6 Mar 2026 00:05:20 +0100 Subject: [PATCH 03/10] Fixes to new api --- content/06-Url-key-extraction.md | 10 +++++----- content/11-URL-building.md | 4 ++-- content/13-Dynamic-content.md | 8 +++++--- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/content/06-Url-key-extraction.md b/content/06-Url-key-extraction.md index 889a404..2137877 100644 --- a/content/06-Url-key-extraction.md +++ b/content/06-Url-key-extraction.md @@ -16,7 +16,7 @@ To get the key properties from a URI an interface ``IKeyFromUriService`` can be public interface IKeyFromUriService { Result GetKeyFromUri(Uri uri) - where THto : HypermediaObject; + where THto : IHypermediaObject; } ``` @@ -26,7 +26,7 @@ Note that since this returns a `Result<>` object, no Exceptions will be thrown, ## Example ```csharp -public class HypermediaCar : HypermediaObject +public class HypermediaCarHto : IHypermediaObject { [Key("id")] public int? Id { get; set; } @@ -47,7 +47,7 @@ public class Parameter : IHypermediaActionParameter Given a HTO definition and the parameter, the key(s) would be extracted like this: ```csharp -var keyResult = keyFromUriService.GetKeyFromUri(parameter.CarUri); +var keyResult = keyFromUriService.GetKeyFromUri(parameter.CarUri); ``` ## Keys from Code Generation @@ -58,7 +58,7 @@ var keyResult = keyFromUriService.GetKeyFromUri +var keyResult = keyFromUriService.GetHypermediaCarKeyFromUri(uri); // Returns Result ``` # URL key extraction (legacy) @@ -157,7 +157,7 @@ The route template: `{brand}/{key:int}`. Be careful to match the variable name a The corresponding post would look like: -``` csharp +``` json [ { "HypermediaActionCustomerBuysCar.Parameter": { diff --git a/content/11-URL-building.md b/content/11-URL-building.md index dc68c43..dd06e18 100644 --- a/content/11-URL-building.md +++ b/content/11-URL-building.md @@ -6,9 +6,9 @@ nav_order: 10 # URL building -Internally RESTyard uses the `LinkGenerator` class to create required URL. To know what endpoint a `HTO` or `Action` have we need so additional information. The RouteAttributes (`HttpGetHypermediaObject`, `HttpDeleteHypermediaAction`, etc.) contain the `HTO` `Action` class (with an optional parameter type). This is why all `HTO` and `Action` have an own type and all routes can in general only reply with one (non error) class. RESTyard needs to be able to do a mapping from `HTO` or `Action` to an endpoint. +Internally RESTyard uses the `LinkGenerator` class to create required URLs. To know what endpoint an HTO or Action has, additional information is needed. The route attributes (`HypermediaObjectEndpoint`, `HypermediaActionEndpoint`, etc.) contain the HTO or Action type. This is why all HTOs and Actions have their own type and all routes can in general only reply with one (non-error) class. RESTyard needs to be able to do a mapping from HTO or Action to an endpoint. -The RESTyard Attributes extend the ASP.Net native attributes e.g. `HttpGet->HttpGetHypermediaObject` or `HttpDelete->HttpDeleteHypermediaAction` with a required Type. This type is used to build a register which maps a `HTO` or a `HypermediaAction` to an endpoint. This allows the RESTyard serializer to find required endpoints and build the URL. +The RESTyard attributes are combined with standard ASP.NET attributes (e.g. `[HttpGet, HypermediaObjectEndpoint]`) and provide the type used to build a register which maps an HTO or a `HypermediaAction` to an endpoint. This allows the RESTyard serializer to find required endpoints and build the URL. {: .highlight } For debugging or logging routes can still have a unique name. If none is given RESTyard will generate one. diff --git a/content/13-Dynamic-content.md b/content/13-Dynamic-content.md index 13743fe..8dccf96 100644 --- a/content/13-Dynamic-content.md +++ b/content/13-Dynamic-content.md @@ -17,7 +17,7 @@ Although the framework is built with the intent of providing type information as In certain scenarios an actions parameters (or not having any) is determined at runtime. This special case can be implemented using `DynamicHypermediaAction`. This action allows for a runtime dynamic schema to be retrieved. To decide what schema is desired it is required to pass runtime values to the schema routes. `DynamicHypermediaAction` has the property `SchemaRouteKeys` which accepts an object which will be passed to route generation so route keys can be filled with values. -This also requires a custom route for the dynamic schemas using `[HttpGetHypermediaActionParameterInfo]`. +This also requires a custom route for the dynamic schemas using `[HypermediaActionParameterInfoEndpoint]`. For this to work the custom type route has to: - have route keys which match the properties in `SchemaRouteKeys` so they can be filled. @@ -29,7 +29,8 @@ Example: ```csharp // Action route, retrieving raw json -[HttpPostHypermediaAction("/MyDynamicAction", typeof(MyDynamicOp))] +[HttpPost("/MyDynamicAction"), + HypermediaActionEndpoint(nameof(MyHto.MyDynamicOp))] public ActionResult MyDynamicAction([FromBody] JsonElement rawObject) { // check rawObject if it matches your dynamic schema if required @@ -38,7 +39,8 @@ public ActionResult MyDynamicAction([FromBody] JsonElement rawObject) } // Dynamic schema route, will work next to default generated ones -[HttpGetHypermediaActionParameterInfo("MyGenericParametersType/{schemaKey1}/{schemaKey2}", typeof(MyGenericParameters))] +[HttpGet("MyGenericParametersType/{schemaKey1}/{schemaKey2}"), + HypermediaActionParameterInfoEndpoint] public ActionResult MyGenericParametersType([FromRoute] string schemaKey1, [FromRoute] string schemaKey2) { // dynamic keys passed to the action are passed here by build URL so the schema can be selected From a55bb7a5aae2061424279902a1ed87919b2dcf9a Mon Sep 17 00:00:00 2001 From: Mathias Reichardt Date: Fri, 6 Mar 2026 00:28:18 +0100 Subject: [PATCH 04/10] Extend docu --- content/01-Key-concepts.md | 73 ++++++++++++++++++++- content/02-Get-started.md | 128 ++++++++++++++++++++++++++++++++++++- content/05-Endpoints.md | 30 ++++++++- content/08-Options.md | 83 ++++++++++++++++++++++-- 4 files changed, 298 insertions(+), 16 deletions(-) diff --git a/content/01-Key-concepts.md b/content/01-Key-concepts.md index 609bb5d..8f6b249 100644 --- a/content/01-Key-concepts.md +++ b/content/01-Key-concepts.md @@ -6,8 +6,75 @@ nav_order: 1 # Key concepts -## Server side ASP.NET +## Siren Hypermedia Format -RESTyard allows you to build a restful web server which responds with Siren documents without building a Siren class and assigning URIs to Links and embedded Entities. For this RESTyard provides two main components: the `HypermediaObject` class and new RouteAttributes extending the Web API RouteAttributes. +RESTyard produces responses in the [Siren](https://github.com/kevinswiber/siren) hypermedia format. A Siren document is a JSON object containing: -HypermediaObjects returned from Controllers will be formatted as Siren. All contained referenced HypermediaObjects (e.g. Links and embedded Entities), Actions, and Parameter types (of Actions) are automatically resolved and properly inserted into the Siren document, by looking up attributed routes. +- **properties** — the entity's data fields (primitives, nested objects, collections) +- **links** — navigational references to other resources (e.g. self, related collections) +- **entities** — embedded sub-entities (inline or as links) +- **actions** — available operations the client can perform (with method, href, and parameter schema) +- **class** — type hints for the client + +Siren's key advantage over plain JSON is that the server tells the client *what it can do next* — available actions, navigation links, and related resources are all part of the response. + +## How RESTyard works + +RESTyard eliminates the need to manually build Siren JSON. Instead, you define C# classes called **HTOs** (Hypermedia Transfer Objects) and the framework handles serialization and URL resolution automatically. + +The flow is: + +1. You define an HTO class implementing `IHypermediaObject` with properties, typed links (`ILink`), and actions (`HypermediaAction`) +2. You create a controller endpoint and annotate it with `HypermediaObjectEndpoint` or `HypermediaActionEndpoint` +3. At startup, RESTyard scans all attributed routes and builds a type-to-endpoint register +4. When a controller returns an HTO via `Ok(myHto)`, the Siren formatter: + - Serializes public properties to Siren `properties` + - Resolves all `ILink` properties to URLs using the route register → Siren `links` + - Resolves all `IEmbeddedEntity` properties → Siren `entities` + - Resolves all `HypermediaAction` properties (where `CanExecute()` is true) to URLs → Siren `actions` + +The result is a fully linked Siren document where clients can discover and navigate the API without hardcoded URLs. + +To see this in action, [RESTyard-HUI](https://github.com/RESTyard/RESTyard-HUI) is a generic web UI that renders any Siren API — it reads the links, entities, and actions from the response and builds a navigable interface automatically. + +## Server side components + +- **`IHypermediaObject`** — marker interface for all HTOs +- **`HypermediaObjectEndpoint`** — route attribute that registers a GET endpoint for an HTO type +- **`HypermediaActionEndpoint`** — route attribute that registers a POST/PUT/PATCH/DELETE endpoint for an action +- **`ILink`** — typed link property, resolved to a URL at serialization time +- **`IEmbeddedEntity`** — typed embedded entity, serialized inline as a full Siren sub-document. Use `List>` for collections, populated with `EmbeddedEntity.Embed()` +- **`HypermediaAction`** — action property with a `CanExecute()` gate and optional parameter type +- **`IHypermediaActionParameter`** — marker interface for action parameter types (records recommended) + +## Controller return patterns + +Controllers use different return methods depending on the situation: + +### `Ok(myHto)` — Return a Siren document + +The standard return for GET endpoints. The Siren formatter serializes the HTO into a full Siren document with resolved links, entities, and actions. + +```csharp +[HttpGet(""), HypermediaObjectEndpoint] +public ActionResult GetRootDocument() +{ + return Ok(customersRoot); +} +``` + +### `this.Created(link)` — Return 201 with Location header + +Used when an action creates a new resource or produces a result the client should navigate to. Returns HTTP 201 with a `Location` header pointing to the created/resulting resource. The client follows this URL to get the resource. + +This is central to the hypermedia approach: **the client never builds URLs** — it receives them from the server. When a client executes an action (e.g. "create customer"), the server tells the client where the result lives via the `Location` header. + +See [Endpoints]({% link content/05-Endpoints.md %}) for the different `Link` variants and examples. + +### Error responses + +RESTyard provides extension methods for common error cases (returning [RFC 7807](https://tools.ietf.org/html/rfc7807) Problem Details): + +- **`this.Problem(problemDetails)`** — Return a `ProblemDetails` response with `application/problem+json` content type +- **`this.CanNotExecute()`** — The requested action cannot be executed (e.g. state has changed since the client received the HTO) +- **`this.UnprocessableEntity()`** — Parameters were technically valid but rejected by business logic diff --git a/content/02-Get-started.md b/content/02-Get-started.md index fd6dc62..c2ae163 100644 --- a/content/02-Get-started.md +++ b/content/02-Get-started.md @@ -5,14 +5,136 @@ nav_order: 2 --- # Get started +{: .no_toc } -To use the Extensions just call `AddHypermediaExtensions()` on your DI container: +
+ + Table of contents + + {: .text-delta } +- TOC +{:toc} +
+ +--- + +## 1. Install the NuGet package + +```bash +dotnet add package RESTyard.AspNetCore +``` + +## 2. Register RESTyard in your DI container + +In `Program.cs`: + +```csharp +builder.Services.AddControllers(); -``` csharp builder.Services.AddHypermediaExtensions(o => { o.ReturnDefaultRouteForUnknownHto = true; // useful during development + // RESTyard scans this assemblies at startup to find all HTO types and attributed routes + o.ControllerAndHypermediaAssemblies = [typeof(Program).Assembly]; }); ``` -To configure the generated URLs in the Hypermedia documents pass a `HypermediaUrlConfig` to `AddHypermediaExtensionsInternal()`. In this way absolute URLs can be generated which have a different scheme or another host e.g. a load balancer. +## 3. Define your first HTO + +Create a simple HTO (Hypermedia Transfer Object) implementing `IHypermediaObject`: + +```csharp +[HypermediaObject(Title = "Entry to the API", Classes = new[] { "Entrypoint" })] +public class EntrypointHto : IHypermediaObject +{ + [Relations([DefaultHypermediaRelations.Self])] + public ILink Self { get; set; } + + [Relations(["Customers"])] + public ILink Customers { get; set; } + + public EntrypointHto() + { + Self = Link.To(this); + Customers = Link.ByKey(null); + } +} + +[HypermediaObject(Title = "The Customers API", Classes = new[] { "CustomersRoot" })] +public class CustomersRootHto : IHypermediaObject +{ + public int TotalCustomers { get; set; } + + [Relations([DefaultHypermediaRelations.Self])] + public ILink Self { get; set; } + + public CustomersRootHto(int totalCustomers) + { + TotalCustomers = totalCustomers; + Self = Link.To(this); + } +} +``` + +## 4. Create controllers with attributed routes + +```csharp +[Route("[controller]")] +[ApiController] +public class EntryPointController : ControllerBase +{ + [HttpGet(""), HypermediaObjectEndpoint] + public ActionResult Get() + { + return Ok(new EntrypointHto()); + } +} + +[Route("Customers")] +[ApiController] +public class CustomersController : ControllerBase +{ + [HttpGet(""), HypermediaObjectEndpoint] + public ActionResult Get() + { + return Ok(new CustomersRootHto(42)); + } +} +``` + +## 5. Run and see Siren output + +```bash +dotnet run +``` + +Request `http://localhost:5000/EntryPoint` with `Accept: application/vnd.siren+json` (or no Accept header). You'll get: + +```json +{ + "class": ["Entrypoint"], + "title": "Entry to the API", + "properties": {}, + "entities": [], + "actions": [], + "links": [ + { + "rel": ["self"], + "href": "http://localhost:5000/EntryPoint" + }, + { + "rel": ["Customers"], + "href": "http://localhost:5000/Customers" + } + ] +} +``` + +All links are automatically resolved from the attributed routes — you never write URL strings. + +## Next steps + +- [HypermediaObject]({% link content/03-HypermediaObject.md %}) — define HTOs with properties, actions, and links +- [Embedded Entities and Links]({% link content/04-Entity-and-Links.md %}) — reference other HTOs +- [Endpoints]({% link content/05-Endpoints.md %}) — route attributes, actions, queries, and file uploads +- [Options]({% link content/08-Options.md %}) — configuration options diff --git a/content/05-Endpoints.md b/content/05-Endpoints.md index 59186b3..a2a3ede 100644 --- a/content/05-Endpoints.md +++ b/content/05-Endpoints.md @@ -29,7 +29,9 @@ The included SirenFormatter will build required links to other routes. At startu This means that for every HTO there must be a route with matching type. -Example from the demo project CustomersRootController: +## Returning a Siren document with `Ok()` + +Use `Ok(myHto)` to return a full Siren document. The formatter serializes the HTO with all resolved links, entities, and actions. ```csharp [HttpGet(""), HypermediaObjectEndpoint] @@ -39,7 +41,9 @@ public ActionResult GetRootDocument() } ``` -The same goes for Actions. The action endpoint references the HTO that owns the action and names the action property: +## Returning a Location header with `this.Created()` + +Action endpoints typically don't return a Siren document. Instead they perform an operation and return HTTP 201 with a `Location` header pointing to the created or resulting resource. The action endpoint references the HTO that owns the action and names the action property: ```csharp [HttpPost("CreateCustomer"), @@ -58,6 +62,22 @@ public async Task NewCustomerAction(CreateCustomerParameters creat } ``` +`this.Created()` returns HTTP 201 with a `Location` header pointing to the created or resulting resource. The client follows this URL instead of building it — this is central to the hypermedia approach. There are several variants: + +```csharp +// Link from an existing HTO instance +return this.Created(Link.To(createdCustomer)); + +// Link by key — no instance needed +return this.Created(Link.ByKey(new HypermediaCarHto.Key(carId, brand))); + +// Link to a query result +return this.Created(Link.ByQuery(query)); +``` + +{: .warning } +When using CORS, you must expose the `Location` header so browser clients can read it: `.WithExposedHeaders("Location")` + {: .highlight } Siren specifies that to trigger an action an array of parameters should be posted to the action route. To avoid wrapping parameters in an array class there is the SingleParameterBinder for convenience. @@ -81,7 +101,11 @@ The parameter binder also allows to pass a parameter object without the wrapping } ``` -Parameters for actions may define a route which provides additional type information to the client. These routes will be added to the Siren fields object as "class". +## Action parameter schemas + +By default, RESTyard automatically generates schema endpoints for all action parameter types (implementing `IHypermediaActionParameter`). These are added to the Siren `fields` object as `class` so clients can discover the expected parameter structure. This is controlled by the `AutoDeliverJsonSchemaForActionParameterTypes` option (see [Options]({% link content/08-Options.md %})). + +Use `HypermediaActionParameterInfoEndpoint` only when you need to override the auto-generated schema with custom logic: ```csharp [HttpGet("NewAddressType"), HypermediaActionParameterInfoEndpoint] diff --git a/content/08-Options.md b/content/08-Options.md index 96b38eb..577a71e 100644 --- a/content/08-Options.md +++ b/content/08-Options.md @@ -5,15 +5,84 @@ nav_order: 8 --- # Options +{: .no_toc } -For some configuration you can pass a `HypermediaExtensionsOptions` object to `AddHypermediaExtensions` method. +
+ + Table of contents + + {: .text-delta } +- TOC +{:toc} +
-- `ReturnDefaultRouteForUnknownHto` if set to `true` the `IHypermediaRouteResolver` will return a default route (see: `DefaultRouteSegmentForUnknownHto`) if a HTO's route is unknown. - This is useful during development time when first writing some HTO's and not all controllers are implemented. Default is `false`. +--- + +Pass a `HypermediaExtensionsOptions` object to `AddHypermediaExtensions` to configure the framework: + +```csharp +builder.Services.AddHypermediaExtensions(o => +{ + o.ReturnDefaultRouteForUnknownHto = true; + o.ControllerAndHypermediaAssemblies = [typeof(EntryPointController).Assembly]; +}); +``` + +## ControllerAndHypermediaAssemblies + +`Assembly[]` — Assemblies to scan for controller routes and HTO types. If none provided, the entry assembly is used. + +Set this explicitly when your HTOs or controllers live in a different assembly than the entry point: + +```csharp +o.ControllerAndHypermediaAssemblies = [typeof(EntryPointController).Assembly]; +``` + +## ReturnDefaultRouteForUnknownHto + +`bool` (default: `false`) — If `true`, the route resolver returns a default route when an HTO type has no corresponding endpoint, instead of throwing an exception. + +Useful during development when writing HTOs before all controllers are implemented. + +## DefaultRouteSegmentForUnknownHto + +`string` (default: `"unknown/object/route"`) — The route segment appended to `:///` when `ReturnDefaultRouteForUnknownHto` is `true`. + +## AutoDeliverJsonSchemaForActionParameterTypes + +`bool` (default: `true`) — Automatically generates routes that deliver JSON schema for action parameter types (implementing `IHypermediaActionParameter`). Custom schema routes created with `HypermediaActionParameterInfoEndpoint` take precedence over auto-generated ones. + +## CaseSensitiveParameterMatching + +`bool` (default: `false`) — Controls whether matching type names for auto-generated parameter schema routes is case-sensitive. + +## ImplicitHypermediaActionParameterBinders + +`bool` (default: `true`) — Automatically adds custom model binders for all action parameters implementing `IHypermediaActionParameter`. This enables the (obsolete) `KeyFromUriAttribute` for those parameter types. + +If `false`, custom binders are only added for parameters explicitly attributed with `[HypermediaActionParameterFromBody]`. See: [URL key extraction]({% link content/06-Url-key-extraction.md %}) + +## HypermediaConverterConfiguration + +Configuration for the Siren serializer: + +- `WriteNullProperties` (`bool`, default: `true`) — If `true`, properties with `null` values are written to the Siren document. Set to `false` to omit them. + +```csharp +o.HypermediaConverterConfiguration = new HypermediaConverterConfiguration +{ + WriteNullProperties = false +}; +``` + +## Replacing built-in services -- `DefaultRouteSegmentForUnknownHto` a `string` which will be appended to `:///` as default route. +Three core services can be replaced by providing an alternative type. The type must implement the corresponding interface: -- `AutoDeliverJsonSchemaForActionParameterTypes`: if set to `true` (default) the routes to action parameters (implementing `IHypermediaActionParameter`) will be generated automatically except when there is a explicit rout created. +- `AlternateRouteRegister` (`Type?`) — Replaces the default `IRouteRegister` implementation. +- `AlternateQueryStringBuilder` (`Type?`) — Replaces the default `IQueryStringBuilder` used to serialize query objects into URL query strings. +- `AlternateJsonSchemaFactory` (`Type?`) — Replaces the default `IJsonSchemaFactory` used to generate JSON schema for action parameters. -- `ImplicitHypermediaActionParameterBinders`: if set to `true` (default) actions receiving a `IHypermediaActionParameter` will have a automatic binder added. - The binder allows to use the (obsolete) ~~`KeyFromUriAttribute`~~ attribute. See: [URL key extraction]({% link content/06-Url-key-extraction.md %}) +```csharp +o.AlternateQueryStringBuilder = typeof(MyCustomQueryStringBuilder); +``` From 3310ccbe97d96657f4980eaf19150ee674283751 Mon Sep 17 00:00:00 2001 From: Mathias Reichardt Date: Fri, 6 Mar 2026 00:41:08 +0100 Subject: [PATCH 05/10] Update relase notes --- content/09-Release-notes.md | 62 +++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/content/09-Release-notes.md b/content/09-Release-notes.md index 969c8ad..328faaf 100644 --- a/content/09-Release-notes.md +++ b/content/09-Release-notes.md @@ -6,6 +6,68 @@ nav_order: 13 # Release Notes +## RESTyard v6.0.2 + +- Fix `KeyFromUriService` throwing on invalid URI input — now returns a proper error instead of an unhandled exception + +## RESTyard v6.0.1 + +- Add `DateTime`, `DateTimeOffset`, and `TimeSpan` JSON schema generators +- Target .NET 8 for the client library +- Switch test assertions to AwesomeAssertions + +## RESTyard v6.0.0 +{: .d-inline-block } + +Breaking +{: .label .label-red } + +- Migrate JSON schema generation from NJsonSchema to [JsonSchema.Net](https://github.com/gregsdennis/json-everything) +- Introduce `IJsonSchemaFactory` interface — can be replaced via `AlternateJsonSchemaFactory` option +- Add `FileUploadHypermediaAction` for file uploads with additional typed parameters +- Add `ExternalFileUploadHypermediaAction` and `ExternalFileUploadHypermediaAction` for external file upload endpoints +- New `HypermediaUploadParameterFromForm` attribute for binding file upload parameters from multipart form data +- Roslyn analyzers included in NuGet package (RY0010–RY0015) to detect deprecated attribute usage and suggest migration + +## RESTyard v5.2.2 + +- Fix invariant string comparison for culture-independent parameter matching +- Add `DateOnly` and `TimeOnly` support in query string serialization + +## RESTyard v5.2.1 + +- Extract pagination logic into reusable `NavigationQuerysBuilder` helper +- Improve query result HTO patterns in CarShack demo + +## RESTyard v5.0.0 +{: .d-inline-block } + +Breaking +{: .label .label-red } + +This is a major API redesign. The old base-class API (`HypermediaObject`, `Links.Add()`, `Entities.Add()`) is replaced with a marker-interface and typed-property API. + +**New API:** +- `IHypermediaObject` marker interface replaces the `HypermediaObject` base class +- `ILink` typed link properties with `[Relations(["..."])]` attribute replace `Links.Add()` +- `List>` with `EmbeddedEntity.Embed()` replaces `Entities.Add()` +- `Link.To()`, `Link.ByKey()`, `Link.ByQuery()`, `Link.External()` factory methods for creating links +- `HypermediaObjectKeyBase` record base for defining key types as nested records +- `this.Created(ILink)` replaces `this.Created(HypermediaObject)` + +**New route attributes:** +- `HypermediaObjectEndpoint` replaces `HttpGetHypermediaObject` +- `HypermediaActionEndpoint(nameof(...))` replaces `HttpPostHypermediaAction`, `HttpDeleteHypermediaAction`, `HttpPatchHypermediaAction` +- `HypermediaActionParameterInfoEndpoint` replaces `HttpGetHypermediaActionParameterInfo` + +The legacy attributes are still supported but deprecated. The included Roslyn analyzers will suggest migration. + +## RESTyard v4.4.0 + +- Switch internal URL generation from `IUrlHelper` to `LinkGenerator` for more reliable route resolution +- Add `KeyFromUriService` for decomposing URLs back into key values (see [URL key extraction]({% link content/06-Url-key-extraction.md %})) +- Add Razor template support in CarShack demo + ## RESTyard v4.3.0 - Added URL deconstruction for list of URIs From d5b75b202d7679cbee3fc264bf40e0677c8ddaa5 Mon Sep 17 00:00:00 2001 From: Mathias Reichardt Date: Fri, 6 Mar 2026 00:44:26 +0100 Subject: [PATCH 06/10] Expand on route design --- content/07-Route-design.md | 135 ++++++++++++++++++++++++++++++++----- 1 file changed, 119 insertions(+), 16 deletions(-) diff --git a/content/07-Route-design.md b/content/07-Route-design.md index afd05b0..01c5b16 100644 --- a/content/07-Route-design.md +++ b/content/07-Route-design.md @@ -5,29 +5,132 @@ nav_order: 7 --- # Recommendations for route design +{: .no_toc } -The extensions were build with some ideas about how routes should be build in mind. The Extensions do not enforce this design but it is useful to know the basic ideas. +
+ + Table of contents + + {: .text-delta } +- TOC +{:toc} +
-- The API is entered by a root document which leads to all or some of the other `HypermediaObject`'s (see `HypermediaEntryPoint` in CarShack) -Examples +--- + +The framework does not enforce a specific route layout, but these conventions work well with RESTyard's link resolution and are used throughout the CarShack demo. + +## Entry point + +Every API should have a single entry point that links to all top-level resources. Clients bookmark this one URL and discover everything else through links. + +``` +GET /entrypoint +``` + +```csharp +[HypermediaObject(Title = "Entry to the API", Classes = new[] { "Entrypoint" })] +public class EntrypointHto : IHypermediaObject +{ + [Relations([DefaultHypermediaRelations.Self])] + public ILink Self { get; set; } + + [Relations(["Customers"])] + public ILink Customers { get; set; } + + [Relations(["Cars"])] + public ILink Cars { get; set; } +} +``` + +The entry point is the only URL a client needs to know. All other URLs are discovered via links. + +## Collection roots + +Collections like "Customers" are accessed through a root HTO, not by returning a list directly. The root HTO handles collection-level actions (create, query) and links to individual entities. + +``` +GET /Customers → CustomersRootHto (links, actions, metadata) +POST /Customers/CreateCustomer → creates a customer, returns Location header +POST /Customers/Queries → creates a query, returns Location header to result +GET /Customers/Query?... → query result with embedded entities +``` + +This avoids returning a potentially large list of entities on the collection URL itself. The client first sees what actions are available, then explicitly requests data through a query action. + +## Entities and actions + +Individual entities are sub-routes of their collection. Actions on an entity are sub-routes of the entity. This ensures route template variables from the entity route are available for its actions. + +``` +GET /Customers/{key} → single customer HTO +POST /Customers/{key}/Move → action on that customer +``` + +{: .highlight } +Actions must be sub-routes of their owning HTO so the framework can fill route template variables. A `Move` action on customer 42 needs the `{key}` variable from `/Customers/{key}`. -```html -http://localhost:5000/entrypoint +## Flat hierarchy + +Do not nest entities under other entities. If a customer has orders, put orders in their own collection rather than under `/Customers/{key}/Orders/{orderId}`. This keeps controllers simple and routes shallow. + +``` +GET /Customers/{key} → customer HTO (with a link to their orders) +GET /Orders/{key} → order HTO ``` -- Collections like `Customers` are accessed through a root object (see `HypermediaCustomersRoot` in CarShack) which handles all actions which are not related to a specific customer. This also avoids that a collection directly answers with potentially unwanted Customers. -Examples +The relationship between customer and orders is expressed through links in the Siren document, not through URL nesting. -```html -http://localhost:5000/Customers -http://localhost:5000/Customers/CreateQuery -http://localhost:5000/Customers/CreateCustomer +## Queries and pagination + +Clients should never build query strings manually. Instead, they post a query object to an action endpoint and follow the `Location` header to the result. + +``` +POST /Customers/Queries → client posts query parameters as JSON + ← 201 Location: /Customers/Query?filter=...&page=0&pageSize=10 +GET /Customers/Query?filter=... → paginated result ``` -- Entities are accessed through a collection but do not host child Entities. These should be handled in their own collections. The routes to the actual objects should not matter, so no need to nest them. This helps to flatten the Controller hierarchy and avoids deep routes. If a placeholder variable is required in the route template name it _key_. -Examples +The query result HTO includes navigation links for pagination: + +```csharp +[Relations(["next"])] +public ILink? Next { get; set; } + +[Relations(["previous"])] +public ILink? Previous { get; set; } -```html -http://localhost:5000/Customers/1 -http://localhost:5000/Customers/1/Move +[Relations(["last"])] +public ILink? Last { get; set; } + +[Relations(["all"])] +public ILink? All { get; set; } +``` + +Nullable links naturally disappear from the Siren output — `Next` is null on the last page, `Previous` is null on the first page. + +## Route template variables + +Use `{key}` as the conventional name for single-key routes. For multi-key routes, use descriptive names matching the HTO's `[Key]` attributes: + +```csharp +// Single key +[HttpGet("{key:int}"), HypermediaObjectEndpoint] + +// Multiple keys +[HttpGet("{brand}/{key:int}"), HypermediaObjectEndpoint] ``` + +See [Endpoints — Routes with a placeholder]({% link content/05-Endpoints.md %}#routes-with-a-placeholder-in-the-route-template) for how to map HTO properties to route variables. + +## Summary + +| Pattern | Route | Purpose | +|---|---|---| +| Entry point | `GET /entrypoint` | Single API entry, links to everything | +| Collection root | `GET /Customers` | Metadata, collection-level actions | +| Create action | `POST /Customers/CreateCustomer` | Returns `Location` to created entity | +| Query action | `POST /Customers/Queries` | Returns `Location` to query result | +| Query result | `GET /Customers/Query?...` | Paginated results with nav links | +| Entity | `GET /Customers/{key}` | Single entity with actions | +| Entity action | `POST /Customers/{key}/Move` | Action on a specific entity | From 4073cae2704beff3e191631be67c1f45468e1c23 Mon Sep 17 00:00:00 2001 From: Mathias Reichardt Date: Fri, 6 Mar 2026 00:56:54 +0100 Subject: [PATCH 07/10] Restructuring documents --- content/01-Key-concepts.md | 3 ++- content/02-Get-started.md | 7 ++++--- content/03-HypermediaObject.md | 7 ++++--- content/04-Entity-and-Links.md | 7 ++++--- content/05-Endpoints.md | 5 +++-- content/06-Url-key-extraction.md | 3 ++- content/07-Route-design.md | 3 ++- content/08-Options.md | 7 ++++--- content/09-Release-notes.md | 5 +++-- content/10-Serialization.md | 7 ++++--- content/11-URL-building.md | 7 ++++--- content/12-Notes-on-Siren.md | 7 ++++--- content/13-Dynamic-content.md | 7 ++++--- content/advanced.md | 10 ++++++++++ content/building-your-api.md | 10 ++++++++++ content/getting-started.md | 10 ++++++++++ content/reference.md | 10 ++++++++++ 17 files changed, 84 insertions(+), 31 deletions(-) create mode 100644 content/advanced.md create mode 100644 content/building-your-api.md create mode 100644 content/getting-started.md create mode 100644 content/reference.md diff --git a/content/01-Key-concepts.md b/content/01-Key-concepts.md index 8f6b249..f0597fb 100644 --- a/content/01-Key-concepts.md +++ b/content/01-Key-concepts.md @@ -1,7 +1,8 @@ --- layout: default title: Key concepts -nav_order: 1 +parent: Getting started +nav_order: 2 --- # Key concepts diff --git a/content/02-Get-started.md b/content/02-Get-started.md index c2ae163..ba233e0 100644 --- a/content/02-Get-started.md +++ b/content/02-Get-started.md @@ -1,7 +1,8 @@ --- layout: default title: Get started -nav_order: 2 +parent: Getting started +nav_order: 1 --- # Get started @@ -135,6 +136,6 @@ All links are automatically resolved from the attributed routes — you never wr ## Next steps - [HypermediaObject]({% link content/03-HypermediaObject.md %}) — define HTOs with properties, actions, and links -- [Embedded Entities and Links]({% link content/04-Entity-and-Links.md %}) — reference other HTOs +- [Links and Embedded Entities]({% link content/04-Entity-and-Links.md %}) — reference other HTOs - [Endpoints]({% link content/05-Endpoints.md %}) — route attributes, actions, queries, and file uploads -- [Options]({% link content/08-Options.md %}) — configuration options +- [Configuration]({% link content/08-Options.md %}) — configuration options diff --git a/content/03-HypermediaObject.md b/content/03-HypermediaObject.md index 89ec465..10b62d7 100644 --- a/content/03-HypermediaObject.md +++ b/content/03-HypermediaObject.md @@ -1,7 +1,8 @@ --- layout: default title: HypermediaObject -nav_order: 3 +parent: Building your API +nav_order: 1 --- # HypermediaObject @@ -90,8 +91,8 @@ public record MarkAsFavoriteParameters(Uri Customer) : IHypermediaActionParamete - Properties which hold a class are recursively serialized (their public properties become nested JSON objects). Note that HTO-specific attributes like `[FormatterIgnoreHypermediaProperty]` are only applied on top-level properties, not inside nested classes. - By default properties which are null will not be added to the Siren document - It is recommended to represent optional values as `Nullable` -- Links to other HTOs are typed `ILink` properties decorated with `[Relations(["..."])]` — see [Embedded Entities and Links]({% link content/04-Entity-and-Links.md %}) -- Embedded entities use `List>` populated with `EmbeddedEntity.Embed()` — see [Embedded Entities and Links]({% link content/04-Entity-and-Links.md %}) +- Links to other HTOs are typed `ILink` properties decorated with `[Relations(["..."])]` — see [Links and Embedded Entities]({% link content/04-Entity-and-Links.md %}) +- Embedded entities use `List>` populated with `EmbeddedEntity.Embed()` — see [Links and Embedded Entities]({% link content/04-Entity-and-Links.md %}) - Properties with a `HypermediaActionBase` type will be added as Actions, but only if `CanExecute()` returns true. Action types derive from `HypermediaAction` (with parameter) or `HypermediaAction` (parameter-less). Required parameters will be added in the "fields" section of the Siren document. - Action parameters implement `IHypermediaActionParameter` (records are recommended) - Properties, Actions and HTOs themselves can be attributed e.g. to give them a fixed name: diff --git a/content/04-Entity-and-Links.md b/content/04-Entity-and-Links.md index 1ce88a8..7de5e40 100644 --- a/content/04-Entity-and-Links.md +++ b/content/04-Entity-and-Links.md @@ -1,10 +1,11 @@ --- layout: default -title: Embedded Entities and Links -nav_order: 4 +title: Links and Embedded Entities +parent: Building your API +nav_order: 2 --- -# Embedded Entities and Links +# Links and Embedded Entities {: .no_toc }
diff --git a/content/05-Endpoints.md b/content/05-Endpoints.md index a2a3ede..c301f55 100644 --- a/content/05-Endpoints.md +++ b/content/05-Endpoints.md @@ -1,7 +1,8 @@ --- layout: default title: Endpoints -nav_order: 5 +parent: Building your API +nav_order: 3 --- # Endpoints @@ -103,7 +104,7 @@ The parameter binder also allows to pass a parameter object without the wrapping ## Action parameter schemas -By default, RESTyard automatically generates schema endpoints for all action parameter types (implementing `IHypermediaActionParameter`). These are added to the Siren `fields` object as `class` so clients can discover the expected parameter structure. This is controlled by the `AutoDeliverJsonSchemaForActionParameterTypes` option (see [Options]({% link content/08-Options.md %})). +By default, RESTyard automatically generates schema endpoints for all action parameter types (implementing `IHypermediaActionParameter`). These are added to the Siren `fields` object as `class` so clients can discover the expected parameter structure. This is controlled by the `AutoDeliverJsonSchemaForActionParameterTypes` option (see [Configuration]({% link content/08-Options.md %})). Use `HypermediaActionParameterInfoEndpoint` only when you need to override the auto-generated schema with custom logic: diff --git a/content/06-Url-key-extraction.md b/content/06-Url-key-extraction.md index 2137877..117c701 100644 --- a/content/06-Url-key-extraction.md +++ b/content/06-Url-key-extraction.md @@ -1,7 +1,8 @@ --- layout: default title: URL key extraction -nav_order: 6 +parent: Advanced +nav_order: 1 --- # URL key extraction diff --git a/content/07-Route-design.md b/content/07-Route-design.md index 01c5b16..dbdeb1c 100644 --- a/content/07-Route-design.md +++ b/content/07-Route-design.md @@ -1,7 +1,8 @@ --- layout: default title: Route design -nav_order: 7 +parent: Building your API +nav_order: 4 --- # Recommendations for route design diff --git a/content/08-Options.md b/content/08-Options.md index 577a71e..e5a440d 100644 --- a/content/08-Options.md +++ b/content/08-Options.md @@ -1,10 +1,11 @@ --- layout: default -title: Options -nav_order: 8 +title: Configuration +parent: Reference +nav_order: 1 --- -# Options +# Configuration {: .no_toc }
diff --git a/content/09-Release-notes.md b/content/09-Release-notes.md index 328faaf..bcca394 100644 --- a/content/09-Release-notes.md +++ b/content/09-Release-notes.md @@ -1,7 +1,8 @@ --- layout: default -title: Release Notes -nav_order: 13 +title: Release notes +parent: Reference +nav_order: 5 --- # Release Notes diff --git a/content/10-Serialization.md b/content/10-Serialization.md index ba6d68d..a804ea7 100644 --- a/content/10-Serialization.md +++ b/content/10-Serialization.md @@ -1,10 +1,11 @@ --- layout: default -title: Notes on serialization -nav_order: 9 +title: Serialization +parent: Reference +nav_order: 2 --- -# Notes on serialization +# Serialization - Enums in `HypermediaObjects` and Action parameters can be attributed using `EnumMember` to specify fixed string names for enum values. If no attribute is present the value will be serialized using `ToString()` - `DateTime` and `DateTimeOffset` will be serialized using ISO 8601 notation: e.g. `2000-11-22T18:05:32.9990000+02:00` diff --git a/content/11-URL-building.md b/content/11-URL-building.md index dd06e18..d09fe6e 100644 --- a/content/11-URL-building.md +++ b/content/11-URL-building.md @@ -1,10 +1,11 @@ --- layout: default -title: URL building -nav_order: 10 +title: URL resolution +parent: Reference +nav_order: 3 --- -# URL building +# URL resolution Internally RESTyard uses the `LinkGenerator` class to create required URLs. To know what endpoint an HTO or Action has, additional information is needed. The route attributes (`HypermediaObjectEndpoint`, `HypermediaActionEndpoint`, etc.) contain the HTO or Action type. This is why all HTOs and Actions have their own type and all routes can in general only reply with one (non-error) class. RESTyard needs to be able to do a mapping from HTO or Action to an endpoint. diff --git a/content/12-Notes-on-Siren.md b/content/12-Notes-on-Siren.md index 5fa71db..bf6b532 100644 --- a/content/12-Notes-on-Siren.md +++ b/content/12-Notes-on-Siren.md @@ -1,10 +1,11 @@ --- layout: default -title: Notes on Siren -nav_order: 11 +title: Deviations from Siren +parent: Reference +nav_order: 4 --- -# Notes on Siren +# Deviations from Siren We use [Siren Hypermedia Format](https://github.com/kevinswiber/siren) for exchange. We like it a lot but have found some limitations which needed us to deviate from the standard. diff --git a/content/13-Dynamic-content.md b/content/13-Dynamic-content.md index 8dccf96..5127bb8 100644 --- a/content/13-Dynamic-content.md +++ b/content/13-Dynamic-content.md @@ -1,10 +1,11 @@ --- layout: default -title: Dynamic content -nav_order: 12 +title: Dynamic actions +parent: Advanced +nav_order: 2 --- -# Dynamic content +# Dynamic actions Although the framework is built with the intent of providing type information as much as possible it can be necessary to deviate from this approach. diff --git a/content/advanced.md b/content/advanced.md new file mode 100644 index 0000000..5d94fa5 --- /dev/null +++ b/content/advanced.md @@ -0,0 +1,10 @@ +--- +layout: default +title: Advanced +nav_order: 3 +has_children: true +--- + +# Advanced + +Specialized features for complex scenarios. diff --git a/content/building-your-api.md b/content/building-your-api.md new file mode 100644 index 0000000..cadbbc6 --- /dev/null +++ b/content/building-your-api.md @@ -0,0 +1,10 @@ +--- +layout: default +title: Building your API +nav_order: 2 +has_children: true +--- + +# Building your API + +Define HTOs, wire up controllers, and design your route structure. diff --git a/content/getting-started.md b/content/getting-started.md new file mode 100644 index 0000000..d1faf0a --- /dev/null +++ b/content/getting-started.md @@ -0,0 +1,10 @@ +--- +layout: default +title: Getting started +nav_order: 1 +has_children: true +--- + +# Getting started + +Get RESTyard running, then understand the concepts behind it. diff --git a/content/reference.md b/content/reference.md new file mode 100644 index 0000000..d3360f5 --- /dev/null +++ b/content/reference.md @@ -0,0 +1,10 @@ +--- +layout: default +title: Reference +nav_order: 4 +has_children: true +--- + +# Reference + +Configuration, serialization details, and internals. From 2f388e21a23a744aa01040848e89db366be543aa Mon Sep 17 00:00:00 2001 From: Mathias Reichardt Date: Fri, 6 Mar 2026 01:04:56 +0100 Subject: [PATCH 08/10] Sepreate action document --- content/02-Get-started.md | 5 +- content/03-HypermediaObject.md | 31 +----- content/04b-Actions.md | 174 +++++++++++++++++++++++++++++++++ content/05-Endpoints.md | 80 +-------------- content/07-Route-design.md | 2 +- 5 files changed, 183 insertions(+), 109 deletions(-) create mode 100644 content/04b-Actions.md diff --git a/content/02-Get-started.md b/content/02-Get-started.md index ba233e0..77581d4 100644 --- a/content/02-Get-started.md +++ b/content/02-Get-started.md @@ -135,7 +135,8 @@ All links are automatically resolved from the attributed routes — you never wr ## Next steps -- [HypermediaObject]({% link content/03-HypermediaObject.md %}) — define HTOs with properties, actions, and links +- [HypermediaObject]({% link content/03-HypermediaObject.md %}) — define HTOs with properties and links - [Links and Embedded Entities]({% link content/04-Entity-and-Links.md %}) — reference other HTOs -- [Endpoints]({% link content/05-Endpoints.md %}) — route attributes, actions, queries, and file uploads +- [Actions]({% link content/04b-Actions.md %}) — define operations clients can perform +- [Endpoints]({% link content/05-Endpoints.md %}) — route attributes, queries, and file uploads - [Configuration]({% link content/08-Options.md %}) — configuration options diff --git a/content/03-HypermediaObject.md b/content/03-HypermediaObject.md index 10b62d7..467f67e 100644 --- a/content/03-HypermediaObject.md +++ b/content/03-HypermediaObject.md @@ -35,39 +35,15 @@ public class HypermediaCustomerHto : IHypermediaObject [Relations([DefaultHypermediaRelations.Self])] public ILink Self { get; set; } - // Actions — only included in Siren output if CanExecute() returns true + // Actions — see the Actions page for details [HypermediaAction(Name = "CustomerMove", Title = "A Customer moved to a new location.")] public CustomerMoveOp CustomerMove { get; set; } - [HypermediaAction(Name = "MarkAsFavorite", Title = "Marks a Customer as a favorite buyer.")] - public MarkAsFavoriteOp MarkAsFavorite { get; set; } - - public HypermediaCustomerHto(int id, string? name, int? age, AddressTo? address, bool isFavorite, - CustomerMoveOp customerMove, MarkAsFavoriteOp markAsFavorite) - { - Id = id; - Name = name; - Age = age; - Address = address; - IsFavorite = isFavorite; - CustomerMove = customerMove; - MarkAsFavorite = markAsFavorite; - PurchaseHistory = Link.ByQuery( - new CustomerPurchaseHistoryQuery(), new CustomerPurchaseHistoryHto.Key(id)); - Self = Link.To(this); - } - - // Action types — derive from HypermediaAction or HypermediaAction (parameter-less) public class CustomerMoveOp : HypermediaAction { public CustomerMoveOp(Func canExecute) : base(canExecute) { } } - public class MarkAsFavoriteOp : HypermediaAction - { - public MarkAsFavoriteOp(Func canExecute) : base(canExecute) { } - } - // Key record for route resolution (see Endpoints page) public record Key(int Id) : HypermediaObjectKeyBase { @@ -78,10 +54,8 @@ public class HypermediaCustomerHto : IHypermediaObject } } -// Action parameters are records implementing IHypermediaActionParameter public record NewAddress(AddressTo Address) : IHypermediaActionParameter; public record AddressTo(string Street, string Number, string City, string ZipCode); -public record MarkAsFavoriteParameters(Uri Customer) : IHypermediaActionParameter; ``` **In short:** @@ -93,8 +67,7 @@ public record MarkAsFavoriteParameters(Uri Customer) : IHypermediaActionParamete - It is recommended to represent optional values as `Nullable` - Links to other HTOs are typed `ILink` properties decorated with `[Relations(["..."])]` — see [Links and Embedded Entities]({% link content/04-Entity-and-Links.md %}) - Embedded entities use `List>` populated with `EmbeddedEntity.Embed()` — see [Links and Embedded Entities]({% link content/04-Entity-and-Links.md %}) -- Properties with a `HypermediaActionBase` type will be added as Actions, but only if `CanExecute()` returns true. Action types derive from `HypermediaAction` (with parameter) or `HypermediaAction` (parameter-less). Required parameters will be added in the "fields" section of the Siren document. -- Action parameters implement `IHypermediaActionParameter` (records are recommended) +- Actions are properties deriving from `HypermediaAction` or `HypermediaAction`, only included if `CanExecute()` returns true — see [Actions]({% link content/04b-Actions.md %}) - Properties, Actions and HTOs themselves can be attributed e.g. to give them a fixed name: - `FormatterIgnoreHypermediaPropertyAttribute` - `HypermediaActionAttribute` diff --git a/content/04b-Actions.md b/content/04b-Actions.md new file mode 100644 index 0000000..1575ed3 --- /dev/null +++ b/content/04b-Actions.md @@ -0,0 +1,174 @@ +--- +layout: default +title: Actions +parent: Building your API +nav_order: 3 +--- + +# Actions +{: .no_toc } + +
+ + Table of contents + + {: .text-delta } +- TOC +{:toc} +
+ +--- + +Actions represent operations a client can perform on an HTO. They appear in the Siren `actions` array — but only when `CanExecute()` returns `true`. + +## Defining an action + +Create a nested class deriving from `HypermediaAction` (or `HypermediaAction` for parameter-less actions). Add it as a property on your HTO: + +```csharp +[HypermediaObject(Title = "A Customer", Classes = new[] { "Customer" })] +public class HypermediaCustomerHto : IHypermediaObject +{ + [HypermediaAction(Name = "CustomerMove", Title = "A Customer moved to a new location.")] + public CustomerMoveOp CustomerMove { get; set; } + + [HypermediaAction(Name = "MarkAsFavorite", Title = "Marks a Customer as a favorite buyer.")] + public MarkAsFavoriteOp MarkAsFavorite { get; set; } + + // Action with parameter + public class CustomerMoveOp : HypermediaAction + { + public CustomerMoveOp(Func canExecute) : base(canExecute) { } + } + + // Action with parameter + public class MarkAsFavoriteOp : HypermediaAction + { + public MarkAsFavoriteOp(Func canExecute) : base(canExecute) { } + } +} +``` + +The `[HypermediaAction]` attribute is optional — use it to give the action a fixed name or title in the Siren output. Without it, the property name is used. + +## Action parameters + +Action parameters implement `IHypermediaActionParameter`. Records are recommended: + +```csharp +public record NewAddress(AddressTo Address) : IHypermediaActionParameter; +public record AddressTo(string Street, string Number, string City, string ZipCode); +``` + +Required properties of the parameter type appear as `fields` in the Siren action. By default, RESTyard auto-generates a JSON schema endpoint for each parameter type so clients can discover the expected structure (see [Configuration]({% link content/08-Options.md %})). + +## CanExecute gate + +The `Func` passed to the constructor controls whether the action appears in the Siren output. This lets the server tell the client what operations are currently available: + +```csharp +// In the controller or service that builds the HTO: +var customerMove = new CustomerMoveOp(canExecute: () => customer.IsActive); +``` + +If `CanExecute()` returns `false`, the action is omitted entirely from the response. + +## Prefilled values + +Actions can supply default values so a client form is pre-populated. Pass them as the second constructor argument: + +```csharp +public class CreateQueryOp : HypermediaAction +{ + public CreateQueryOp(Func canExecute, CustomerQuery? prefilledValues = default) + : base(canExecute, prefilledValues) + { + } +} +``` + +Prefilled values are serialized into the Siren `fields` as `value`. + +## Parameter-less actions + +For actions without parameters, derive from `HypermediaAction` (no type parameter): + +```csharp +public class MarkAsDoneOp : HypermediaAction +{ + public MarkAsDoneOp(Func canExecute) : base(canExecute) { } +} +``` + +## File upload actions + +Use `FileUploadHypermediaAction` or `FileUploadHypermediaAction` for actions that accept file uploads. Pass `FileUploadConfiguration` to inform the client about allowed behavior (file size, type, single vs. multiple): + +```csharp +public class UploadCarImageOp : FileUploadHypermediaAction +{ + public UploadCarImageOp(Func canExecute, FileUploadConfiguration? fileUploadConfiguration = null) + : base(canExecute, fileUploadConfiguration) + { + } +} +``` + +See [Endpoints — File upload actions]({% link content/05-Endpoints.md %}#file-upload-actions) for the controller-side setup. + +## External actions + +Use `HypermediaExternalAction` or `HypermediaExternalAction` for actions that call an external API instead of a local endpoint: + +```csharp +public class ExternalActionNoParameters : HypermediaExternalAction +{ + public ExternalActionNoParameters(Uri externalUri, string httpMethod) + : base(() => true, externalUri, httpMethod) { } +} + +public class ExternalActionWithArgument : HypermediaExternalAction +{ + public ExternalActionWithArgument( + Uri externalUri, + string httpMethod, + string acceptedMediaType, + ExternalActionParameters prefilledValues) + : base(() => true, externalUri, httpMethod, acceptedMediaType, prefilledValues) { } +} + +public class ExternalActionParameters : IHypermediaActionParameter +{ + public int AInt { get; } + public ExternalActionParameters(int aInt) { AInt = aInt; } +} + +// Usage in HTO: +public ExternalActionNoParameters ExternalAction { get; init; } + = new ExternalActionNoParameters(new Uri("http://www.example.com"), HttpMethod.POST); + +public ExternalActionWithArgument ExternalActionWithArgs { get; init; } + = new ExternalActionWithArgument( + new Uri("http://www.example2.com"), + HttpMethod.DELETE, + "application/json", + new ExternalActionParameters(3)); +``` + +External actions render with the external URI as `href` in the Siren output instead of a locally resolved route. + +## Dynamic actions + +If an action's parameters (or whether it has any) are determined at runtime, use `DynamicHypermediaAction`. See [Dynamic actions]({% link content/13-Dynamic-content.md %}) for details. + +## Action types summary + +| Base class | Use case | +|---|---| +| `HypermediaAction` | Parameter-less action | +| `HypermediaAction` | Action with typed parameters | +| `FileUploadHypermediaAction` | File upload without extra parameters | +| `FileUploadHypermediaAction` | File upload with extra parameters | +| `HypermediaExternalAction` | External API call, no parameters | +| `HypermediaExternalAction` | External API call with parameters | +| `DynamicHypermediaAction` | Runtime-determined parameters | diff --git a/content/05-Endpoints.md b/content/05-Endpoints.md index c301f55..4a0eb92 100644 --- a/content/05-Endpoints.md +++ b/content/05-Endpoints.md @@ -2,7 +2,7 @@ layout: default title: Endpoints parent: Building your API -nav_order: 3 +nav_order: 4 --- # Endpoints @@ -119,39 +119,13 @@ public ActionResult NewAddressType() Also see: [URL key extraction]({% link content/06-Url-key-extraction.md %}) -## Actions with prefilled values - -Actions can supply prefilled values so a form is already filled with server provided content. Actions with parameters have an optional parameter: - -```csharp -public class CreateQueryOp : HypermediaAction -{ - public CreateQueryOp(Func canExecute, CustomerQuery? prefilledValues = default) - : base(canExecute, prefilledValues) - { - } -} -``` - -## Actions with acceptable media type - -The `HypermediaActionEndpoint` attribute is combined with standard HTTP method attributes. To specify an acceptable media type, use the `DefaultMediaTypes` constants: - -```csharp -[HttpPost("UploadImage"), - HypermediaActionEndpoint(nameof(HypermediaCarsRootHto.UploadCarImage), DefaultMediaTypes.MultipartFormData)] -``` - -Will be rendered to Siren as `type` on the action. Default is `application/json`. - ## File upload actions -Use the `FileUploadHypermediaAction` or `ExternalFileUploadHypermediaAction` to specify a file upload. Pass `FileUploadConfiguration` to send information to the client about allowed behavior. +Use `FileUploadHypermediaAction` or `FileUploadHypermediaAction` for file uploads (see [Actions — File upload actions]({% link content/04b-Actions.md %}#file-upload-actions) for the action definition). The endpoint uses `DefaultMediaTypes.MultipartFormData`: Controller example: ```csharp -// controller [HttpPost("UploadImage"), HypermediaActionEndpoint(nameof(HypermediaCarsRootHto.UploadCarImage), DefaultMediaTypes.MultipartFormData)] public async Task UploadCarImage( @@ -159,18 +133,9 @@ public async Task UploadCarImage( HypermediaFileUploadActionParameter uploadParameters) { var files = uploadParameters.Files; // Access uploaded files - var additionalParameter = uploadParameters.ParameterObject; // Access generic additional parameter + var additionalParameter = uploadParameters.ParameterObject; // Access additional typed parameter //... } - -// action definition -public class UploadCarImageOp : FileUploadHypermediaAction -{ - public UploadCarImageOp(Func canExecute, FileUploadConfiguration? fileUploadConfiguration = null) - : base(canExecute, fileUploadConfiguration) - { - } -} ``` Files are uploaded using `multipart/form-data`. The additional parameter is added as a serialized json string to the key-value-dictionary of the form. @@ -199,45 +164,6 @@ hco.UploadCarImage.ExecuteAsync( Note that the name and filename are mandatory in order for the file to be recognized as a file and not as a "normal" parameter. -## Calling external APIs using Actions - -If it is necessary to call an external API and expose that call as an action there is `HypermediaExternalAction` and `HypermediaExternalAction` to be used as base for action types. - -```csharp -public class ExternalActionNoParameters : HypermediaExternalAction -{ - public ExternalActionNoParameters(Uri externalUri, string httpMethod) - : base(() => true, externalUri, httpMethod) { } -} - -public class ExternalActionWithArgument : HypermediaExternalAction -{ - public ExternalActionWithArgument( - Uri externalUri, - string httpMethod, - string acceptedMediaType, - ExternalActionParameters prefilledValues) - : base(() => true, externalUri, httpMethod, acceptedMediaType, prefilledValues) { } -} - -public class ExternalActionParameters : IHypermediaActionParameter -{ - public int AInt { get; } - public ExternalActionParameters(int aInt) { AInt = aInt; } -} - -// usage in HTO: -public ExternalActionNoParameters ExternalAction { get; init; } - = new ExternalActionNoParameters(new Uri("http://www.example.com"), HttpMethod.POST); - -public ExternalActionWithArgument ExternalActionWithArgs { get; init; } - = new ExternalActionWithArgument( - new Uri("http://www.example2.com"), - HttpMethod.DELETE, - "application/json", - new ExternalActionParameters(3)); -``` - ## Routes with a placeholder in the route template For access to entities a route template may contain placeholder variables like _key_ in the example below. diff --git a/content/07-Route-design.md b/content/07-Route-design.md index dbdeb1c..7c98a7c 100644 --- a/content/07-Route-design.md +++ b/content/07-Route-design.md @@ -2,7 +2,7 @@ layout: default title: Route design parent: Building your API -nav_order: 4 +nav_order: 5 --- # Recommendations for route design From 2173f436dc118d8b5dbbe33d9ee3b1ce69e69c26 Mon Sep 17 00:00:00 2001 From: Mathias Reichardt Date: Fri, 6 Mar 2026 01:10:41 +0100 Subject: [PATCH 09/10] Polish --- content/01-Key-concepts.md | 12 ++++++++++++ content/06-Url-key-extraction.md | 15 +++++++++++++-- content/07-Route-design.md | 2 +- content/09-Release-notes.md | 2 +- 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/content/01-Key-concepts.md b/content/01-Key-concepts.md index f0597fb..7b380ec 100644 --- a/content/01-Key-concepts.md +++ b/content/01-Key-concepts.md @@ -6,6 +6,18 @@ nav_order: 2 --- # Key concepts +{: .no_toc } + +
+ + Table of contents + + {: .text-delta } +- TOC +{:toc} +
+ +--- ## Siren Hypermedia Format diff --git a/content/06-Url-key-extraction.md b/content/06-Url-key-extraction.md index 117c701..bfdc6e2 100644 --- a/content/06-Url-key-extraction.md +++ b/content/06-Url-key-extraction.md @@ -6,11 +6,22 @@ nav_order: 1 --- # URL key extraction -{: .d-inline-block } +{: .no_toc .d-inline-block } (v4.4.0) {: .label .label-green } +
+ + Table of contents + + {: .text-delta } +- TOC +{:toc} +
+ +--- + When it is required to reference an other resource as action parameter it is often required to get the identifying keys from the resources URL. To get the key properties from a URI an interface ``IKeyFromUriService`` can be obtained from the DI container: ```csharp @@ -82,7 +93,7 @@ public class FavoriteCustomer : IHypermediaActionParameter } ``` -- The first parameter `typeof(HypermediaCustomer)` gives the expected `HypermediaObject` so the frame work knows which route layout it should use, and there to what kind of Resource the provided URL should lead. +- The first parameter `typeof(HypermediaCustomer)` gives the expected `HypermediaObject` so the framework knows which route layout it should use, and to what kind of resource the provided URL should lead. - The second parameter `schemaProperyName: "Customer"` is to identify the property which holds the URL in the payload JSON object. {: .highlight } diff --git a/content/07-Route-design.md b/content/07-Route-design.md index 7c98a7c..0180140 100644 --- a/content/07-Route-design.md +++ b/content/07-Route-design.md @@ -5,7 +5,7 @@ parent: Building your API nav_order: 5 --- -# Recommendations for route design +# Route design {: .no_toc }
diff --git a/content/09-Release-notes.md b/content/09-Release-notes.md index bcca394..5c23709 100644 --- a/content/09-Release-notes.md +++ b/content/09-Release-notes.md @@ -5,7 +5,7 @@ parent: Reference nav_order: 5 --- -# Release Notes +# Release notes ## RESTyard v6.0.2 From 321975391e008cfd1c8d20005bcc2ea0969d6a1c Mon Sep 17 00:00:00 2001 From: Mathias Reichardt Date: Fri, 6 Mar 2026 08:59:48 +0100 Subject: [PATCH 10/10] Fix readme since docker command did not work anymore --- Gemfile.lock | 7 +++++-- README.md | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3f65e13..7eed022 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -14,6 +14,7 @@ GEM eventmachine (1.2.7) ffi (1.17.1-arm64-darwin) ffi (1.17.1-x86_64-linux-gnu) + ffi (1.17.1-x86_64-linux-musl) forwardable-extended (2.6.0) google-protobuf (4.29.3-arm64-darwin) bigdecimal @@ -76,9 +77,10 @@ GEM rexml (3.4.2) rouge (4.5.1) safe_yaml (1.0.5) - sass-embedded (1.83.4-arm64-darwin) + sass-embedded (1.83.4) google-protobuf (~> 4.29) - sass-embedded (1.83.4-x86_64-linux-gnu) + rake (>= 13) + sass-embedded (1.83.4-arm64-darwin) google-protobuf (~> 4.29) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -88,6 +90,7 @@ GEM PLATFORMS arm64-darwin x86_64-linux-gnu + x86_64-linux-musl DEPENDENCIES jekyll (~> 4.4.1) diff --git a/README.md b/README.md index e1f92dc..c887a5d 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ **Deployed here [https://restyard.github.io/RESTyard-Docs/](https://restyard.github.io/RESTyard-Docs/)** -RUN build: `docker run --rm --volume="${PWD}:/srv/jekyll:Z" -it jekyll/jekyll:latest /bin/bash -c "bundle install && bundle exec jekyll"` -Preview: show with `http_server` in _site folder (node tool) +RUN build: `docker run --rm --volume="${PWD}:/srv/jekyll:Z" -w /srv/jekyll -it ruby:3.3-slim /bin/bash -c "apt-get update && apt-get install -y build-essential && bundle install && bundle exec jekyll build"` +Preview: `docker run --rm --volume="${PWD}:/srv/jekyll:Z" -w /srv/jekyll -p 4000:4000 -it ruby:3.3-slim /bin/bash -c "apt-get update && apt-get install -y build-essential && bundle install && bundle exec jekyll serve --host 0.0.0.0 --livereload"` This is a *bare-minimum* template to create a [Jekyll] site that: