Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -88,6 +90,7 @@ GEM
PLATFORMS
arm64-darwin
x86_64-linux-gnu
x86_64-linux-musl

DEPENDENCIES
jekyll (~> 4.4.1)
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
88 changes: 84 additions & 4 deletions content/01-Key-concepts.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,93 @@
---
layout: default
title: Key concepts
nav_order: 1
parent: Getting started
nav_order: 2
---

# Key concepts
{: .no_toc }

## Server side APS.NET
<details open markdown="block">
<summary>
Table of contents
</summary>
{: .text-delta }
- TOC
{:toc}
</details>

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.
---

## Siren Hypermedia Format

RESTyard produces responses in the [Siren](https://github.com/kevinswiber/siren) hypermedia format. A Siren document is a JSON object containing:

- **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<T>`), and actions (`HypermediaAction<T>`)
2. You create a controller endpoint and annotate it with `HypermediaObjectEndpoint<THto>` or `HypermediaActionEndpoint<THto>`
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<T>` properties to URLs using the route register → Siren `links`
- Resolves all `IEmbeddedEntity<T>` 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<THto>`** — route attribute that registers a GET endpoint for an HTO type
- **`HypermediaActionEndpoint<THto>`** — route attribute that registers a POST/PUT/PATCH/DELETE endpoint for an action
- **`ILink<THto>`** — typed link property, resolved to a URL at serialization time
- **`IEmbeddedEntity<THto>`** — typed embedded entity, serialized inline as a full Siren sub-document. Use `List<IEmbeddedEntity<THto>>` for collections, populated with `EmbeddedEntity.Embed<T>()`
- **`HypermediaAction<TParameter>`** — 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<HypermediaCustomersRootHto>]
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):

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.
- **`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
132 changes: 128 additions & 4 deletions content/02-Get-started.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,142 @@
---
layout: default
title: Get started
nav_order: 2
parent: Getting started
nav_order: 1
---

# Get started
{: .no_toc }

To use the Extensions just call `AddHypermediaExtensions()` on your DI container:
<details open markdown="block">
<summary>
Table of contents
</summary>
{: .text-delta }
- TOC
{:toc}
</details>

---

## 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<EntrypointHto> Self { get; set; }

[Relations(["Customers"])]
public ILink<CustomersRootHto> Customers { get; set; }

public EntrypointHto()
{
Self = Link.To(this);
Customers = Link.ByKey<CustomersRootHto>(null);
}
}

[HypermediaObject(Title = "The Customers API", Classes = new[] { "CustomersRoot" })]
public class CustomersRootHto : IHypermediaObject
{
public int TotalCustomers { get; set; }

[Relations([DefaultHypermediaRelations.Self])]
public ILink<CustomersRootHto> 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<EntrypointHto>]
public ActionResult Get()
{
return Ok(new EntrypointHto());
}
}

[Route("Customers")]
[ApiController]
public class CustomersController : ControllerBase
{
[HttpGet(""), HypermediaObjectEndpoint<CustomersRootHto>]
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 and links
- [Links and Embedded Entities]({% link content/04-Entity-and-Links.md %}) — reference other HTOs
- [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
Loading